Compare commits

..
92 Commits
Author SHA1 Message Date
FreddleSpl0it 6c877fc0ac Merge pull request #7520 from mailcow/feat/update-ui
Feat/update UI
2026-10-15 15:31:18 +02:00
FreddleSpl0it 4a29767a31 [Web] Add role color marker and icons to login pages 2026-10-15 15:23:29 +02:00
FreddleSpl0it cd29426933 [Web] Fix minor customize and theme ui issues 2026-10-15 15:15:42 +02:00
FreddleSpl0it 37be306f1c [Web] change default theme label 2026-10-15 15:10:49 +02:00
FreddleSpl0it a351017455 [Web] Update bootstrap-select css to 1.14.0-beta3 2026-10-15 14:47:19 +02:00
FreddleSpl0it 887b8a3594 [Web] Add servercow theme 2026-10-15 14:46:57 +02:00
FreddleSpl0it 571c1b5b29 [Web] Update Bootstrap to 5.3.8, add theme support 2026-10-15 14:43:06 +02:00
FreddleSpl0it 8cd231c9b9 Merge pull request #7505 from mailcow/feat/rspamd-4.2.1
[Rspamd] Update to 4.2.1
2026-10-06 10:02:49 +02:00
FreddleSpl0it 74facc4b5b [Rspamd] update to 4.2.1 2026-10-06 09:41:51 +02:00
FreddleSpl0it ff6329d01b [Rspamd] Fix CTE and boundary prefix in domain wide footer 2026-10-06 09:41:34 +02:00
FreddleSpl0it e8209d6504 Merge pull request #7504 from ralfbergs/fix/typo-resovler
Fixed typo (changing RESOVLER to RESOLVER).
2026-10-06 09:21:33 +02:00
FreddleSpl0it 626d006a3d Merge pull request #7484 from oidipos/feat/tlspol
[postfix-tlspol] bump to v1.14.0
2026-10-06 09:20:47 +02:00
Ralf Bergs e88c734d0a Fixed typo (changing RESOVLER to RESOLVER). 2026-10-03 12:30:32 +02:00
milkmaker 86bba266f6 update postscreen_access.cidr (#7494) 2026-10-01 08:03:05 +02:00
oidipos e3ed8d2fc2 bump postfix-tlspol to v1.14.0 2026-09-25 21:23:58 +02:00
FreddleSpl0it 88d92261e4 Merge pull request #7432 from mailcow/renovate/redis-7.x
Update redis Docker tag to v7.4.11
2026-09-21 09:32:35 +02:00
FreddleSpl0it 017d0c7ff0 Merge pull request #7475 from mailcow/feat/sogo-5.12.11
[SOGo] Update to 5.12.11
2026-09-21 08:46:32 +02:00
FreddleSpl0it a28fa19044 [SOGo] Update to 5.12.11 2026-09-21 08:43:16 +02:00
FreddleSpl0it fa2ad14fc4 Merge pull request #7473 from mailcow/feat/unbound-1.26.1
[Unbound] Update to 1.26.1
2026-09-21 08:29:13 +02:00
FreddleSpl0it a480a96e10 [Unbound] Update to 1.26.1 2026-09-21 08:23:03 +02:00
renovate[bot] 42d387ca42 Update redis Docker tag to v7.4.11
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2026-09-15 16:52:39 +00:00
milkmakerandStefano fe4e675754 [Web] Updated lang.it-it.json (#7460)
Co-authored-by: Stefano <stefano.vassena@gmail.com>
2026-09-09 22:28:28 +02:00
b05f59b705 Translations update from Weblate (#7457)
* [Web] Updated lang.si-si.json

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.zh-cn.json

Co-authored-by: 雨 <luotianyi@luotianyi.me>

---------

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: 雨 <luotianyi@luotianyi.me>
2026-09-07 21:38:12 +02:00
milkmaker c4d0ca56d2 update postscreen_access.cidr (#7450) 2026-09-01 13:07:54 +02:00
7ec61fde8a Translations update from Weblate (#7431)
* [Web] Updated lang.it-it.json

Co-authored-by: Claudio <cb@bastard.it>

* [Web] Updated lang.si-si.json

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>

---------

Co-authored-by: Claudio <cb@bastard.it>
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2026-08-19 20:23:04 +02:00
FreddleSpl0it 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
FreddleSpl0it 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
FreddleSpl0it 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
FreddleSpl0it 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
FreddleSpl0it 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
FreddleSpl0it 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
FreddleSpl0it 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
FreddleSpl0it 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
FreddleSpl0it 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
FreddleSpl0it 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
FreddleSpl0it 555e7ed706 [Redis] Update to 7.4.10 2026-08-18 08:12:32 +02:00
FreddleSpl0it 59e2a9ac77 Merge pull request #7423 from mailcow/fix/7418
[Dovecot] Remove legacy DeltaChat auto-filing sieve rule
2026-08-17 08:41:18 +02:00
FreddleSpl0it feed5ad183 [Dovecot] Remove legacy DeltaChat auto-filing sieve rule 2026-08-17 08:35:53 +02:00
FreddleSpl0it f1f7a9800b Merge pull request #7422 from mailcow/feat/sogo-5.12.10
[SOGo] Update to 5.12.10
2026-08-17 08:24:10 +02:00
FreddleSpl0it 8c85044781 [SOGo] Update to 5.12.10 2026-08-17 08:20:45 +02:00
FreddleSpl0it 8fb32e4b59 Merge pull request #7415 from mailcow/feat/clamd-1.4.6
[Clamd] Update to 1.4.6
2026-08-13 13:59:13 +02:00
FreddleSpl0it b5fa926bc0 [Clamd] Update to 1.4.6 2026-08-13 13:56:49 +02:00
d1a2f4e168 Translations update from Weblate (#7400)
* [Web] Updated lang.si-si.json

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: milkmaker <milkmaker@mailcow.de>

* [Web] Updated lang.pt-br.json

Co-authored-by: André Glazastov <andre@glazastov.com>

---------

Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
Co-authored-by: André Glazastov <andre@glazastov.com>
2026-08-04 18:37:38 +02:00
milkmaker f7a536d634 update postscreen_access.cidr (#7394) 2026-08-04 18:35:48 +02:00
Maximal Benedikt 8b456a33e7 Merge pull request #7392 from mailcow/update-sponsoring
update README.md sponsors
2026-07-30 14:21:39 +02:00
MaximalBenedikt 54da526d41 update README.md sponsors 2026-07-30 14:11:32 +02:00
FreddleSpl0it 50be483c39 Merge pull request #7390 from mailcow/fix/6859
[ACME] Skip mta-sts certificate request when MTA-STS is not active for a domain
2026-07-30 10:44:24 +02:00
FreddleSpl0it d64c923aca [ACME] Skip mta-sts certificate request when MTA-STS is not active for a domain 2026-07-30 10:33:55 +02:00
FreddleSpl0it 2d5f166ba8 Merge pull request #7389 from mailcow/fix/default-mbox-template
[Web] Create default mailbox template with eas and dav access
2026-07-30 09:23:24 +02:00
FreddleSpl0it 94b5a623ab Merge pull request #7388 from mailcow/fix/7329
[Web] Move mailcow update check to server side
2026-07-30 09:13:19 +02:00
FreddleSpl0it e406ecd461 Merge pull request #7387 from mailcow/fix/mfk25
Hardening mailcow
2026-07-30 09:07:10 +02:00
FreddleSpl0it 68a95c3af3 Merge pull request #7386 from mailcow/feat/rspamd-4.1.4
[Rspamd] update to 4.1.4
2026-07-30 08:59:04 +02:00
FreddleSpl0it bd037d5644 [Nginx] Set image tag to 1.30.4-1 2026-07-30 08:51:21 +02:00
FreddleSpl0it 7036dbf4f8 [Rspamd] update to 4.1.4 2026-07-30 08:50:18 +02:00
FreddleSpl0it 2f10a4633f Merge pull request #7385 from mailcow/fix/cors
[Web] harden CORS origin matching and add Vary: Origin
2026-07-30 08:41:01 +02:00
FreddleSpl0it 36c70db86c [Web] harden CORS origin matching and add Vary: Origin 2026-07-30 08:37:18 +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
FreddleSpl0it f4961c4023 Merge pull request #7333 from fallmo/fix/cors-settings-validation
fix: cors allowed origins settings validation
2026-07-28 15:35:29 +02:00
FreddleSpl0it 38de21592c Merge pull request #7358 from SYNLINQ/staging
Fix nginx CVE-2026-42533
2026-07-28 14:40:30 +02:00
FreddleSpl0it f44bd2f36a [Web] document sender_acl in get/mailbox API examples 2026-07-28 14:19:45 +02:00
FreddleSpl0it 95a77f2dcb Merge pull request #7348 from smpaz7467/fix/api-get-mailbox-sender-acl
[Web] return sender_acl in get/mailbox API
2026-07-28 14:12:57 +02:00
FreddleSpl0it e856510fb2 Merge pull request #7345 from smpaz7467/fix/time-limited-alias-api
[Web] fix add/time_limited_alias silently discarding requests and validity
2026-07-28 13:49:37 +02:00
FreddleSpl0it d51d06d716 Merge pull request #7343 from smpaz7467/fix/nginx-ipv6-default-server
[Nginx] only bind IPv6 default_server when ENABLE_IPV6 is set
2026-07-28 13:35:17 +02:00
FreddleSpl0it 54170d075a [Web] Move mailcow update check to server side 2026-07-28 13:32:18 +02:00
renovate[bot] b4bb1a625b Update actions/stale action to v11 (#7375) 2026-07-28 11:07:20 +02:00
FreddleSpl0it 2e5a29bf69 Merge pull request #7367 from oidipos/fix/quarantine-subject
fix: restore subject display in quarantine overview
2026-07-28 10:54:23 +02:00
FreddleSpl0it e245ac04d9 [Web] enforce tenant boundary for SOGo SSO 2026-07-28 09:21:29 +02:00
oidipos c877fdf0a5 fix: remove MIME decoding of JSON encoded subject
Since moving to rspamd's multipart metadata_exporter,
`subject` is a JSON encoded UTF-8 string and must not be MIME decoded.
2026-07-24 21:03:00 +02:00
FreddleSpl0it fea38c8e1b [Web] escape mailbox name 2026-07-24 11:16:39 +02:00
FreddleSpl0it cc9af65852 [Web] remove domain admin sso token after use 2026-07-24 10:26:23 +02:00
FreddleSpl0it 92cc8bec90 [Web] Use parameterized LIKE for sogo_acl deletion 2026-07-23 15:29:41 +02:00
FreddleSpl0it 145745329e [Web] add dot stuffing for quarantine raw release 2026-07-23 15:01:44 +02:00
FreddleSpl0it 8e72d22c56 [Web] Escape rspamd_history and rllog 2026-07-23 11:17:55 +02:00
FreddleSpl0it ddd76d99cd [Web] Add sogo_auth_internal nginx marker for sogo-auth.php 2026-07-23 10:10:29 +02:00
SYNLINQ 8c9524a2fe Update docker-compose.yml
reference patched nginx image
2026-07-20 21:32:57 +02:00
SYNLINQ 2ebd32d2ee Update Dockerfile
bump nginx version to 1.30.4
2026-07-20 21:31:57 +02:00
FreddleSpl0it 4b62af9d02 update README.md sponsors 2026-07-15 08:29:46 +02: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 14772c3a20 [Web] return sender_acl in get/mailbox API
sender_acl can be set through edit/mailbox but was never returned by
get/mailbox, so an API client could not read back what it had written,
and get/mailbox/all / get/mailbox/{mailbox} both omitted it.

Add the mailbox's internal send-as ACL (the sender_acl table rows with
external = 0) to mailbox_details as sender_acl, an array of send_as
values, mirroring the field edit/mailbox accepts. It is added in the same
block as the other detailed fields, so the lightweight get/mailbox/reduced
endpoint is unaffected.

Fixes #7011

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:36:40 -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 c17cc8a792 [Web] fix add/time_limited_alias silently discarding requests and validity
Three defects in add/time_limited_alias:

The description was read as $_data['description'] without a guard. When a
client omits it, null is bound to spamalias.description, which is TEXT NOT
NULL, so the insert raises a PDOException. The global exception handler is
terminal, so process_add_return() never echoes anything and the caller sees
HTTP 200 with an empty body while no alias was created. Default it to an
empty string instead.

The validity guard used a single condition whose else branch also caught the
success case, so every valid validity was overwritten with the 8760 hour
default and the parameter did nothing. Only invalid values were rejected.
Nest the range check so a valid value survives.

The OpenAPI spec documented only username and domain, while the code also
reads description, validity and permanent. Spec driven clients therefore
could not construct a working request. Document all three.

spamalias.description is the only NOT NULL description column in the schema,
which is why the same unguarded read in add/domain and add/resource does not
fail, both of those columns are nullable.

This does not change the generic exception handling. A database error is
still swallowed into an empty HTTP 200, and the message the handler builds
carries the raw PDOException, so surfacing it to API clients would need
sanitising first. That is left for a separate change.

Refs #7287

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:23:31 -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
Stephen RitzandClaude Opus 4.8 7dab6c63d1 [Nginx] only bind IPv6 default_server when ENABLE_IPV6 is set
The HTTP-to-HTTPS redirect server block bound `listen [::]:{{ HTTP_PORT }}
default_server` unconditionally, while every other IPv6 listen directive in
this template is guarded by `{% if ENABLE_IPV6 %}`. On hosts where IPv6 is
disabled at the kernel level, nginx cannot bind `::` and fails to start.

This only triggers when HTTP_REDIRECT is enabled and ENABLE_IPV6 is false,
which is why it survives testing with ENABLE_IPV6=false alone.

Guard the directive like the other six IPv6 listeners in the template.

Refs #7296

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:54:28 -07:00
Paul Seebach 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 Seebach 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
Mohamed Fall 6f7fee49cd fix: cors allowed origins settings validation 2026-07-13 15:29:48 +00: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
FreddleSpl0it 8ead057ce9 [Web] Create default mailbox template with eas and dav access 2026-04-15 09:33:23 +02:00
78 changed files with 5817 additions and 3304 deletions

No files matched your search

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Mark/Close Stale Issues and Pull Requests 🗑️
uses: actions/stale@v10.4.0
uses: actions/stale@v11.0.0
with:
repo-token: ${{ secrets.STALE_ACTION_PAT }}
days-before-stale: 60
+2
View File
@@ -64,6 +64,8 @@ data/web/templates/cache/*
!data/web/templates/cache/.gitkeep
data/web/.well-known/acme-challenge
data/web/css/build/0081-custom-mailcow.css
data/web/css/themes/*/
!data/web/css/themes/servercow/
data/web/inc/vars.local.inc.php
data/web/inc/app_info.inc.php
data/web/nextcloud*/
+5
View File
@@ -23,10 +23,15 @@ A big thank you to everyone supporting us on GitHub Sponsors—your contribution
<a href="https://www.maehdros.com/" target=_blank><img
src="https://avatars.githubusercontent.com/u/173894712" height="58"
/></a>
<a href="https://garske-systems.de/" target=_blank><img
src="https://garske-systems.de/img/logo_min.png" height="58"
/></a>
### 50$/Month Sponsors
<a href="https://github.com/vnukhr" target=_blank><img
src="https://avatars.githubusercontent.com/u/7805987?s=52&v=4" height="58"
/></a><a href="https://github.com/humiico" target=_blank><img
src="https://avatars.githubusercontent.com/u/110134705?s=52&v=4" height="58"
/></a>
## Info, documentation and support
+21
View File
@@ -249,12 +249,33 @@ while true; do
done <<< "${SQL_DOMAINS}"
if [[ ${ONLY_MAILCOW_HOSTNAME} != "y" ]]; then
# Fetch all domains with an active MTA-STS policy once.
unset MTA_STS_ACTIVE_DOMAINS
declare -A MTA_STS_ACTIVE_DOMAINS
if [[ ${AUTODISCOVER_SAN} == "y" ]]; then
SQL_MTA_STS_DOMAINS=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT domain FROM mta_sts WHERE active = 1" -Bs)
if [[ $? -eq 0 ]]; then
while read mta_sts_domain; do
if [[ -z "${mta_sts_domain}" ]]; then
# ignore empty lines
continue
fi
MTA_STS_ACTIVE_DOMAINS["${mta_sts_domain}"]=1
done <<< "${SQL_MTA_STS_DOMAINS}"
fi
fi
for SQL_DOMAIN in "${SQL_DOMAIN_ARR[@]}"; do
unset VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
declare -a VALIDATED_CONFIG_DOMAINS_SUBDOMAINS
for SUBDOMAIN in "${ADDITIONAL_WC_ARR[@]}"; do
FULL_SUBDOMAIN="${SUBDOMAIN}.${SQL_DOMAIN}"
# Skip mta-sts subdomain unless MTA-STS is enabled (active) for this domain
if [[ "${SUBDOMAIN}" == "mta-sts" && -z "${MTA_STS_ACTIVE_DOMAINS[${SQL_DOMAIN}]}" ]]; then
log_f "MTA-STS is not enabled for ${SQL_DOMAIN} - skipping mta-sts subdomain certificate"
continue
fi
# Skip if subdomain matches MAILCOW_HOSTNAME
if [[ "${FULL_SUBDOMAIN}" == "${MAILCOW_HOSTNAME}" ]]; then
continue
+3 -3
View File
@@ -1,7 +1,7 @@
FROM alpine:3.21 AS builder
FROM alpine:3.24 AS builder
WORKDIR /src
ENV CLAMD_VERSION=1.4.2
ENV CLAMD_VERSION=1.4.6
RUN apk upgrade --no-cache \
&& apk add --update --no-cache \
@@ -68,7 +68,7 @@ RUN wget -P /src https://www.clamav.net/downloads/production/clamav-${CLAMD_VERS
"/clamav/etc/clamav/clamav-milter.conf.sample" > "/clamav/etc/clamav/clamav-milter.conf" || exit 1
FROM alpine:3.21
FROM alpine:3.24
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM nginx:1.30.3-alpine
FROM nginx:1.30.4-alpine
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ENV PIP_BREAK_SYSTEM_PACKAGES=1
+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.14.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 {
+1 -1
View File
@@ -2,7 +2,7 @@ FROM debian:trixie-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_4.1.0-1~e2b0b18
ARG RSPAMD_VER=rspamd_4.2.1-1~b7a16ae
ARG CODENAME=trixie
ENV LC_ALL=C
+3 -3
View File
@@ -1,6 +1,6 @@
# SOGo built from source to enable security patch application
# Repository: https://github.com/Alinto/sogo
# Version: SOGo-5.12.9
# Version: SOGo-5.12.11
#
# Applied security patches:
# -
@@ -12,8 +12,8 @@ FROM debian:bookworm
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_VERSION=SOGo-5.12.9
ARG SOPE_VERSION=SOPE-5.12.9
ARG SOGO_VERSION=SOGo-5.12.11
ARG SOPE_VERSION=SOPE-5.12.11
# Security patches to apply (space-separated commit hashes)
ARG SOGO_SECURITY_PATCHES=""
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
+2 -1
View File
@@ -3,7 +3,8 @@ FROM alpine:3.23
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# install unbound from alpine:edge to get security patches
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main unbound
# minimum version is pinned
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main "unbound>=1.26.1-r0"
# install other packages from regular alpine stable repo
RUN apk add --update --no-cache \
+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
-10
View File
@@ -1,13 +1,3 @@
# global_sieve_before script
# global_sieve_before -> user sieve_before (mailcow UI) -> user sieve_after (mailcow UI) -> global_sieve_after
require ["mailbox", "fileinto"];
if header :contains ["Chat-Version"] [""] {
if mailboxexists "DeltaChat" {
fileinto "DeltaChat";
} else {
fileinto :create "DeltaChat";
}
stop;
}
+7
View File
@@ -42,12 +42,19 @@ http {
https https;
}
map $server_port $sogo_auth_internal {
65510 "1";
default "";
}
{% if HTTP_REDIRECT %}
# HTTP to HTTPS redirect
server {
root /web;
listen {{ HTTP_PORT }} default_server;
{% if ENABLE_IPV6 %}
listen [::]:{{ HTTP_PORT }} default_server;
{%endif%}
server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }};
@@ -116,6 +116,9 @@ 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;
fastcgi_send_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 "*"
+88 -38
View File
@@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Wed Jul 1 00:51:20 UTC 2026
# Whitelist generated by Postwhite v3.4 on Thu Oct 1 00:35:20 UTC 2026
# https://github.com/stevejenkins/postwhite/
# 2251 total rules
# 2246 total rules
2a00:1450:4000::/36 permit
2a00:1450:4864::/56 permit
2a01:111:f400::/48 permit
@@ -29,6 +29,7 @@
2a01:b747:3004:200::/56 permit
2a01:b747:3005:200::/56 permit
2a01:b747:3006:200::/56 permit
2a01:b747:3007::/56 permit
2a02:a60:0:5::/64 permit
2a0f:f640::/56 permit
2c0f:fb50:4000::/36 permit
@@ -56,7 +57,14 @@
8.25.194.0/23 permit
8.25.196.0/23 permit
8.36.116.0/24 permit
8.39.54.0/23 permit
8.39.54.240/29 permit
8.39.54.250/31 permit
8.39.144.0/24 permit
8.40.222.0/23 permit
8.40.222.240/29 permit
8.40.222.250/31 permit
8.47.10.9 permit
12.130.86.238 permit
13.108.16.0/20 permit
13.110.208.0/21 permit
@@ -67,6 +75,7 @@
13.111.191.0/24 permit
13.216.7.111 permit
13.216.54.180 permit
13.247.164.219 permit
15.200.21.50 permit
15.200.44.248 permit
15.200.201.185 permit
@@ -116,7 +125,6 @@
23.249.208.0/20 permit
23.251.224.0/19 permit
23.253.141.0/24 permit
23.253.182.0/23 permit
23.253.182.103 permit
23.253.183.145 permit
23.253.183.146 permit
@@ -138,24 +146,7 @@
27.123.206.80/28 permit
31.47.251.17 permit
31.186.239.0/24 permit
34.2.64.0/22 permit
34.2.68.0/23 permit
34.2.70.0/23 permit
34.2.71.64/26 permit
34.2.72.0/22 permit
34.2.75.0/26 permit
34.2.78.0/23 permit
34.2.80.0/23 permit
34.2.82.0/23 permit
34.2.84.0/24 permit
34.2.84.64/26 permit
34.2.85.0/24 permit
34.2.85.64/26 permit
34.2.86.0/23 permit
34.2.88.0/23 permit
34.2.90.0/23 permit
34.2.92.0/23 permit
34.2.94.0/23 permit
34.2.64.0/19 permit
34.70.158.162 permit
34.74.74.140 permit
34.83.159.189 permit
@@ -170,6 +161,7 @@
34.215.104.144 permit
34.218.115.239 permit
34.225.212.172 permit
34.241.242.183 permit
35.83.148.184 permit
35.155.198.111 permit
35.158.23.94 permit
@@ -181,6 +173,7 @@
35.205.92.9 permit
35.228.216.85 permit
35.242.169.159 permit
37.187.220.204 permit
37.188.97.188 permit
37.218.248.47 permit
37.218.249.47 permit
@@ -193,6 +186,7 @@
40.233.64.216 permit
40.233.83.78 permit
40.233.88.28 permit
43.239.212.33 permit
44.206.138.57 permit
44.210.169.44 permit
44.217.45.156 permit
@@ -253,8 +247,8 @@
46.228.39.112/31 permit
46.243.88.174 permit
46.243.88.175 permit
46.243.88.176 permit
46.243.88.177 permit
46.243.88.176/30 permit
46.243.95.96/27 permit
46.243.95.179 permit
46.243.95.180 permit
50.16.246.183 permit
@@ -272,9 +266,11 @@
50.56.130.221 permit
50.56.130.222 permit
50.112.246.219 permit
50.152.146.25 permit
51.83.17.38 permit
52.1.14.157 permit
52.5.230.59 permit
52.6.74.205 permit
52.12.53.23 permit
52.13.214.179 permit
52.26.1.71 permit
@@ -331,6 +327,7 @@
54.244.54.130 permit
54.244.242.0/24 permit
54.255.61.23 permit
56.124.6.228 permit
57.103.64.0/18 permit
57.129.93.249 permit
62.13.128.0/24 permit
@@ -361,6 +358,7 @@
64.127.115.252 permit
64.132.88.0/23 permit
64.132.92.0/24 permit
64.181.136.241 permit
64.181.194.190 permit
64.181.213.254 permit
64.207.219.7 permit
@@ -397,6 +395,7 @@
65.110.161.77 permit
65.123.29.213 permit
65.123.29.220 permit
65.154.166.0/24 permit
65.212.180.36 permit
66.102.0.0/20 permit
66.119.150.192/26 permit
@@ -640,6 +639,7 @@
79.135.106.0/24 permit
79.135.107.0/24 permit
80.225.160.128/25 permit
81.88.38.0/23 permit
81.169.146.243 permit
81.169.146.245 permit
81.169.146.246 permit
@@ -724,6 +724,7 @@
96.43.148.64/28 permit
96.43.148.64/31 permit
96.43.151.64/28 permit
98.77.0.0/16 permit
98.97.248.0/21 permit
98.136.44.181 permit
98.136.44.182/31 permit
@@ -1210,14 +1211,15 @@
99.78.197.208/28 permit
103.9.96.0/22 permit
103.28.42.0/24 permit
103.84.217.15 permit
103.84.217.238 permit
103.89.75.238 permit
103.151.192.0/23 permit
103.168.172.128/27 permit
103.237.104.0/22 permit
104.43.243.237 permit
104.44.112.128/25 permit
104.47.0.0/17 permit
104.130.96.0/28 permit
104.130.122.0/23 permit
106.10.144.64/27 permit
106.10.144.100/31 permit
106.10.144.103 permit
@@ -1370,6 +1372,10 @@
117.120.16.0/21 permit
119.42.242.52/31 permit
119.42.242.156 permit
121.244.91.31 permit
121.244.91.48 permit
121.244.91.52 permit
122.15.156.182 permit
123.126.78.64/29 permit
124.108.96.24/31 permit
124.108.96.28/31 permit
@@ -1435,6 +1441,7 @@
130.35.116.0/25 permit
130.61.9.72 permit
130.162.39.83 permit
130.162.194.23 permit
130.248.172.0/24 permit
130.248.173.0/24 permit
131.186.12.0/25 permit
@@ -1445,14 +1452,27 @@
132.226.49.32 permit
132.226.56.24 permit
134.98.248.128/25 permit
134.128.64.0/19 permit
134.128.96.0/19 permit
134.128.64.0/18 permit
134.170.27.8 permit
134.170.113.0/26 permit
134.170.141.64/26 permit
134.170.143.0/24 permit
134.170.174.0/24 permit
135.84.80.0/24 permit
135.84.81.0/24 permit
135.84.82.0/24 permit
135.84.83.0/24 permit
135.84.216.0/22 permit
136.143.160.0/24 permit
136.143.161.0/24 permit
136.143.162.0/24 permit
136.143.176.0/24 permit
136.143.177.0/24 permit
136.143.178.49 permit
136.143.182.0/23 permit
136.143.184.0/24 permit
136.143.188.0/24 permit
136.143.190.0/23 permit
136.146.128.0/20 permit
136.147.128.0/20 permit
136.147.135.0/24 permit
@@ -1472,6 +1492,7 @@
139.138.46.219 permit
139.138.57.55 permit
139.138.58.119 permit
139.167.79.86 permit
139.177.108.0/25 permit
139.180.17.0/24 permit
140.238.148.191 permit
@@ -1503,9 +1524,6 @@
146.20.14.105 permit
146.20.14.106 permit
146.20.14.107 permit
146.20.112.0/26 permit
146.20.113.0/24 permit
146.20.191.0/24 permit
146.20.215.0/24 permit
146.20.215.182 permit
146.88.28.0/24 permit
@@ -1528,10 +1546,11 @@
149.97.173.180 permit
149.118.160.128/25 permit
150.136.21.199 permit
150.171.109.183 permit
150.171.109.178 permit
150.230.98.160 permit
151.145.38.14 permit
152.67.105.195 permit
152.69.165.251 permit
152.69.200.236 permit
152.70.155.126 permit
155.248.135.128/25 permit
@@ -1553,6 +1572,8 @@
158.247.16.0/20 permit
158.247.100.0/25 permit
159.13.4.0/25 permit
159.13.33.181 permit
159.26.176.0/20 permit
159.92.154.0/24 permit
159.92.155.0/24 permit
159.92.157.0/24 permit
@@ -1580,6 +1601,8 @@
159.183.121.182 permit
159.183.129.172 permit
160.1.62.192 permit
161.33.94.110 permit
161.33.228.244 permit
161.38.192.0/20 permit
161.38.204.0/22 permit
161.71.32.0/19 permit
@@ -1611,7 +1634,6 @@
165.173.182.0/24 permit
165.173.182.250/31 permit
165.173.189.205 permit
166.78.68.0/22 permit
166.78.68.221 permit
166.78.69.169 permit
166.78.69.170 permit
@@ -1643,6 +1665,19 @@
168.245.12.252 permit
168.245.46.9 permit
168.245.127.231 permit
169.148.129.0/24 permit
169.148.131.0/24 permit
169.148.138.0/24 permit
169.148.142.10 permit
169.148.142.33 permit
169.148.144.0/25 permit
169.148.144.10 permit
169.148.146.0/23 permit
169.148.174.10 permit
169.148.175.3 permit
169.148.179.3 permit
169.148.188.0/24 permit
169.148.188.182 permit
170.9.232.254 permit
170.10.128.0/24 permit
170.10.129.0/24 permit
@@ -1672,6 +1707,8 @@
175.41.215.51 permit
176.32.105.0/24 permit
176.32.127.0/24 permit
176.94.34.32/27 permit
176.94.34.64/27 permit
178.236.10.128/26 permit
182.50.76.0/22 permit
182.50.78.64/28 permit
@@ -1753,9 +1790,9 @@
188.125.85.234/31 permit
188.125.85.236/31 permit
188.125.85.238 permit
188.165.51.139 permit
188.172.128.0/20 permit
192.0.64.0/18 permit
192.9.166.184 permit
192.18.139.154 permit
192.18.145.36 permit
192.18.152.58 permit
@@ -1780,7 +1817,6 @@
192.30.252.0/22 permit
192.161.144.0/20 permit
192.162.87.0/24 permit
192.237.158.0/23 permit
192.237.159.42 permit
192.237.159.43 permit
192.254.112.0/20 permit
@@ -1823,7 +1859,6 @@
198.21.0.0/21 permit
198.37.144.0/20 permit
198.37.152.186 permit
198.61.254.0/23 permit
198.61.254.21 permit
198.61.254.231 permit
198.178.234.57 permit
@@ -1849,7 +1884,16 @@
199.16.156.0/22 permit
199.33.145.1 permit
199.33.145.32 permit
199.34.22.36 permit
199.59.148.0/22 permit
199.67.80.2 permit
199.67.80.20 permit
199.67.82.2 permit
199.67.82.20 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
199.67.90.0/24 permit
199.101.161.130 permit
199.101.162.0/25 permit
199.122.120.0/21 permit
@@ -1901,12 +1945,17 @@
204.14.232.0/21 permit
204.14.232.64/28 permit
204.14.234.64/28 permit
204.75.18.128/27 permit
204.75.142.0/24 permit
204.92.114.187 permit
204.92.114.203 permit
204.92.114.204/31 permit
204.141.32.0/23 permit
204.141.42.0/23 permit
204.216.164.202 permit
204.220.160.0/21 permit
204.220.90.0/23 permit
204.220.92.0/22 permit
204.220.160.0/20 permit
204.220.168.0/21 permit
204.220.176.0/20 permit
204.220.181.105 permit
@@ -1942,6 +1991,8 @@
207.211.31.0/25 permit
207.211.41.113 permit
207.211.132.0/25 permit
207.211.147.87 permit
207.211.159.110 permit
207.218.90.0/24 permit
207.218.90.122 permit
207.250.68.0/24 permit
@@ -1991,7 +2042,6 @@
209.43.22.0/28 permit
209.46.117.168 permit
209.46.117.179 permit
209.61.151.0/24 permit
209.61.151.236 permit
209.61.151.249 permit
209.61.151.251 permit
@@ -2178,7 +2228,7 @@
2001:748:400:3301::4 permit
2404:6800:4000::/36 permit
2404:6800:4864::/56 permit
2603:1061:14:1c2::1 permit
2603:1061:14:1d0::1 permit
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
+6 -4
View File
@@ -856,6 +856,8 @@ rspamd_config:register_symbol({
local seen_cte
local newline_s = newline(task)
-- use the CTE add_text_footer actually applied
local new_cte = rewrite.new_cte or 'quoted-printable'
local function rewrite_ct_cb(name, hdr)
if rewrite.need_rewrite_ct then
@@ -877,13 +879,13 @@ rspamd_config:register_symbol({
return
elseif name:lower() == 'content-transfer-encoding' then
out[#out + 1] = string.format('%s: %s',
'Content-Transfer-Encoding', 'quoted-printable')
'Content-Transfer-Encoding', new_cte)
-- update Content-Transfer-Encoding header
task:set_milter_reply({
remove_headers = {['Content-Transfer-Encoding'] = 0},
})
task:set_milter_reply({
add_headers = {['Content-Transfer-Encoding'] = 'quoted-printable'}
add_headers = {['Content-Transfer-Encoding'] = new_cte}
})
seen_cte = true
return
@@ -895,7 +897,7 @@ rspamd_config:register_symbol({
task:headers_foreach(rewrite_ct_cb, {full = true})
if not seen_cte and rewrite.need_rewrite_ct then
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', 'quoted-printable')
out[#out + 1] = string.format('%s: %s', 'Content-Transfer-Encoding', new_cte)
end
-- End of headers
@@ -914,7 +916,7 @@ rspamd_config:register_symbol({
out_parts[#out_parts + 1] = o
out_parts[#out_parts + 1] = newline_s
else
local removePrefix = "--\x0D\x0AContent-Type"
local removePrefix = "--\x0D\x0AContent-"
if string.lower(string.sub(tostring(o[1]), 1, string.len(removePrefix))) == string.lower(removePrefix) then
o[1] = string.sub(tostring(o[1]), string.len("--\x0D\x0A") + 1)
end
+8 -8
View File
@@ -53,7 +53,7 @@ $raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
$raw_size = (int)$_FILES['message']['size'];
$qid = $meta['qid'] ?? 'unknown';
$subject = iconv_mime_decode($meta['subject'] ?? '');
$subject = $meta['subject'] ?? '';
$score = $meta['score'] ?? 0;
$rcpts = $meta['rcpt'] ?? array();
$user = $meta['user'] ?? 'unknown';
@@ -153,12 +153,12 @@ foreach ($rcpts as $rcpt) {
// Loop through all found gotos
foreach ($gotos_array as $index => &$goto) {
error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
$stmt->execute(array(':goto' => $goto));
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
if (!empty($username)) {
error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: mailbox found: " . $username . PHP_EOL);
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
if (!in_array($username, $rcpt_final_mailboxes)) {
$rcpt_final_mailboxes[] = $username;
@@ -167,21 +167,21 @@ foreach ($rcpts as $rcpt) {
else {
$parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
error_log("RCPT RESOLVER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
}
else {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
$stmt->execute(array(':goto' => $goto));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if ($goto_branch) {
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch);
} else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {
error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
}
}
@@ -203,11 +203,11 @@ foreach ($rcpts as $rcpt) {
// Force exit if loop cannot be solved
// Postfix does not allow for alias loops, so this should never happen.
$loop_c++;
error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
}
}
catch (PDOException $e) {
error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
error_log("RCPT RESOLVER: " . $e->getMessage() . PHP_EOL);
http_response_code(502);
exit;
}
+8 -8
View File
@@ -50,7 +50,7 @@ $qid = $meta['qid'] ?? 'unknown';
$rcpts = $meta['rcpt'] ?? array();
$sender = $meta['from'] ?? '';
$ip = $meta['ip'] ?? 'unknown';
$subject = iconv_mime_decode($meta['subject'] ?? '');
$subject = $meta['subject'] ?? '';
$messageid= $meta['message_id'] ?? '';
$priority = 0;
@@ -139,12 +139,12 @@ foreach ($rcpts as $rcpt) {
// Loop through all found gotos
foreach ($gotos_array as $index => &$goto) {
error_log("RCPT RESOVLER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: query " . $goto . " as username from mailbox" . PHP_EOL);
$stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :goto AND (`active`= '1' OR `active`= '2');");
$stmt->execute(array(':goto' => $goto));
$username = $stmt->fetch(PDO::FETCH_ASSOC)['username'];
if (!empty($username)) {
error_log("RCPT RESOVLER: http pipe: mailbox found: " . $username . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: mailbox found: " . $username . PHP_EOL);
// Current goto is a mailbox, save to rcpt_final_mailboxes if not a duplicate
if (!in_array($username, $rcpt_final_mailboxes)) {
$rcpt_final_mailboxes[] = $username;
@@ -153,21 +153,21 @@ foreach ($rcpts as $rcpt) {
else {
$parsed_goto = parse_email($goto);
if (!$redis->hGet('DOMAIN_MAP', $parsed_goto['domain'])) {
error_log("RCPT RESOVLER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
error_log("RCPT RESOLVER:" . $goto . " is not a mailcow handled mailbox or alias address" . PHP_EOL);
}
else {
$stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :goto AND `active` = '1'");
$stmt->execute(array(':goto' => $goto));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['goto'];
if ($goto_branch) {
error_log("RCPT RESOVLER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: goto address " . $goto . " is an alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = explode(',', $goto_branch);
} else {
$stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain AND `active` = '1'");
$stmt->execute(array(':domain' => $parsed_goto['domain']));
$goto_branch = $stmt->fetch(PDO::FETCH_ASSOC)['target_domain'];
if ($goto_branch) {
error_log("RCPT RESOVLER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: goto domain " . $parsed_goto['domain'] . " is a domain alias branch for " . $goto_branch . PHP_EOL);
$goto_branch_array = array($parsed_goto['local'] . '@' . $goto_branch);
}
}
@@ -189,11 +189,11 @@ foreach ($rcpts as $rcpt) {
// Force exit if loop cannot be solved
// Postfix does not allow for alias loops, so this should never happen.
$loop_c++;
error_log("RCPT RESOVLER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
error_log("RCPT RESOLVER: http pipe: goto array count on loop #". $loop_c . " is " . count($gotos_array) . PHP_EOL);
}
}
catch (PDOException $e) {
error_log("RCPT RESOVLER: " . $e->getMessage() . PHP_EOL);
error_log("RCPT RESOLVER: " . $e->getMessage() . PHP_EOL);
http_response_code(502);
exit;
}
+2
View File
@@ -115,6 +115,8 @@ $template_data = [
'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
'ip_check' => customize('get', 'ip_check'),
'custom_login' => customize('get', 'custom_login'),
'ui_themes' => customize('get', 'ui_themes'),
'ui_theme' => customize('get', 'ui_theme'),
'password_complexity' => password_complexity('get'),
'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
'cors_settings' => $cors_settings,
+16 -2
View File
@@ -185,6 +185,9 @@ paths:
example:
username: info@domain.tld
domain: domain.tld
description: my time limited alias
validity: 8760
permanent: false
properties:
username:
description: 'the mailbox an alias should be created for'
@@ -192,6 +195,15 @@ paths:
domain:
description: "the domain"
type: string
description:
description: "a description for the alias, defaults to an empty string"
type: string
validity:
description: "how many hours the alias stays valid, 1 to 87600, defaults to 8760 (one year)"
type: integer
permanent:
description: "keep the alias after it expired, defaults to false"
type: boolean
type: object
summary: Create time limited alias
/api/v1/add/app-passwd:
@@ -4908,6 +4920,7 @@ paths:
quota: 3221225472
quota_used: 0
rl: false
sender_acl: ["otherbox@doman3.tld", "@aliasdomain.tld"]
spam_aliases: 0
username: info@doman3.tld
tags: ["tag1", "tag2"]
@@ -5834,6 +5847,7 @@ paths:
quota: 3221225472
quota_used: 0
rl: false
sender_acl: ["otherbox@domain3.tld", "@aliasdomain.tld"]
spam_aliases: 0
username: info@domain3.tld
tags: ["tag1", "tag2"]
@@ -5856,7 +5870,7 @@ paths:
response:
value:
- type: "success"
log: ["cors", "edit", {"allowed_origins": ["*", "mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
log: ["cors", "edit", {"allowed_origins": ["*", "https://mail.mailcow.tld"], "allowed_methods": ["POST", "GET", "DELETE", "PUT"]}]
msg: "cors_headers_edited"
description: OK
headers: { }
@@ -5873,7 +5887,7 @@ paths:
schema:
example:
attr:
allowed_origins: ["*", "mail.mailcow.tld"]
allowed_origins: ["*", "https://mail.mailcow.tld"]
allowed_methods: ["POST", "GET", "DELETE", "PUT"]
properties:
attr:
+8 -9
View File
@@ -1,10 +1,10 @@
/*!
* Bootstrap-select v1.14.0-beta2 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2021 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
/*!
* Bootstrap-select v1.14.0-beta3 (https://developer.snapappointments.com/bootstrap-select)
*
* Copyright 2012-2022 SnapAppointments, LLC
* Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE)
*/
@-webkit-keyframes bs-notify-fadeOut {
0% {
opacity: 0.9;
@@ -35,8 +35,7 @@ select.selectpicker {
display: none !important;
}
.bootstrap-select {
width: 220px \0;
/*IE9 and below*/
width: 220px;
vertical-align: middle;
}
.bootstrap-select > .dropdown-toggle {
+5 -5
View File
@@ -17,15 +17,15 @@ table.dataTable>tbody>tr.child ul.dtr-details>li {
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
background-color: #5e5e5e;
background-color: var(--mc-dtr-hover-bg);
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before,
table.dataTable td.dt-control:before {
background-color: #979797 !important;
border: 1.5px solid #616161 !important;
background-color: var(--mc-dtr-bg) !important;
border: 1.5px solid var(--mc-dtr-border) !important;
border-radius: 2px !important;
color: #fff;
color: var(--mc-dtr-color);
height: 1em;
width: 1em;
line-height: 1.25em;
@@ -37,7 +37,7 @@ table.dataTable td.dt-control:before {
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before,
table.dataTable td.dt-control:before {
background-color: #979797 !important;
background-color: var(--mc-dtr-active-bg) !important;
}
table.dataTable.table-striped>tbody>tr>td {
vertical-align: middle;
+82 -32
View File
@@ -34,11 +34,44 @@
url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff');
}
/* theme tokens, override in css/themes/<name>/theme.css */
:root {
--mc-font-family: "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
--mc-font-size: 10.5pt;
--mc-body-bg: #fbfbfb;
--mc-primary: #0F688D;
--mc-primary-border: #0d526d;
--mc-info: #148DBC;
--mc-info-border: #127ea8;
--mc-border: #dfdfdf;
--mc-surface: #fff;
--mc-surface-alt: #f7f7f7;
--mc-muted: #959595;
--mc-hr: #ccc;
--mc-progress-bg: #d5d5d5;
--mc-placeholder: rgb(137 137 137);
--mc-btn-outline-color: #000000;
--mc-btn-outline-border: #cfcfcf;
--mc-btn-outline-hover-bg: #f0f0f0;
--mc-btn-outline-active-bg: #f0f0f0;
--mc-btn-light-active-color: #fff;
--mc-btn-light-active-bg: #555;
--mc-btn-light-active-border: #4d4d4d;
--mc-dtr-bg: #979797;
--mc-dtr-border: #616161;
--mc-dtr-color: #fff;
--mc-dtr-hover-bg: #5e5e5e;
--mc-dtr-active-bg: #979797;
--mc-login-user: #28b62c;
--mc-login-domainadmin: #ffc107;
--mc-login-admin: #ff4136;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: #fbfbfb;
background-color: var(--mc-body-bg);
}
#maxmsgsize { min-width: 80px; }
#slider1 .slider-selection {
@@ -86,17 +119,17 @@ body {
max-height: 1000px
}
.nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
border-color: #dfdfdf;
border-color: var(--mc-border);
}
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
border-color: #dfdfdf;
border-bottom: 1px solid #ffffff;
border-color: var(--mc-border);
border-bottom: 1px solid var(--mc-surface);
}
.nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus {
border-color: #dfdfdf;
border-color: var(--mc-border);
}
.nav-tabs {
border-bottom: 1px solid #dfdfdf;
border-bottom: 1px solid var(--mc-border);
}
.bi {
display: inline-block;
@@ -164,13 +197,13 @@ body.modal-open {
padding-right: inherit !important;
}
body {
font-family: "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 10.5pt;
font-family: var(--mc-font-family);
font-size: var(--mc-font-size);
line-height: 1.5;
}
html {
font-family: "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 10.5pt;
font-family: var(--mc-font-family);
font-size: var(--mc-font-size);
line-height: 1.5;
}
#mailcow-alert {
@@ -206,7 +239,7 @@ legend {
cursor: not-allowed;
}
.overlay {
background: #fff;
background: var(--mc-surface);
position: absolute;
z-index: 10000;
top: 0; right: 0; bottom: 0; left: 0;
@@ -215,9 +248,6 @@ legend {
.bootstrap-select.btn-group .no-results {
display: none;
}
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
color: rgb(197, 197, 197) !important;
}
.haveibeenpwned {
cursor: pointer;
-webkit-user-select: none;
@@ -239,7 +269,7 @@ legend {
.footer {
margin-top: 27px;
margin-bottom: 20px;
color: #959595;
color: var(--mc-muted);
display: flex;
flex-direction: column;
}
@@ -337,7 +367,7 @@ code {
overflow-x: auto!important;
}
.well {
border: 1px solid #dfdfdf;
border: 1px solid var(--mc-border);
background-color: #f9f9f9;
padding: 10px;
}
@@ -356,47 +386,47 @@ button[aria-expanded='true'] > .caret {
}
.list-group-details {
background: #fff;
background: var(--mc-surface);
}
.list-group-header {
background: #f7f7f7;
background: var(--mc-surface-alt);
}
.bg-primary, .alert-primary, .btn-primary {
background-color: #0F688D !important;
border-color: #0d526d !important;
background-color: var(--mc-primary) !important;
border-color: var(--mc-primary-border) !important;
}
.bg-info, .alert-info, .btn-info {
background-color: #148DBC !important;
border-color: #127ea8 !important;
background-color: var(--mc-info) !important;
border-color: var(--mc-info-border) !important;
}
.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
color: rgb(137 137 137)!important;
color: var(--mc-placeholder) !important;
}
.progress {
height: 16px;
background-color: #d5d5d5;
background-color: var(--mc-progress-bg);
}
.btn-outline-secondary:hover {
background-color: #f0f0f0;
background-color: var(--mc-btn-outline-hover-bg);
}
.btn.btn-outline-secondary {
color: #000000 !important;
border-color: #cfcfcf !important;
color: var(--mc-btn-outline-color) !important;
border-color: var(--mc-btn-outline-border) !important;
}
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
background-color: #f0f0f0 !important;
background-color: var(--mc-btn-outline-active-bg) !important;
}
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
color: #fff;
background-color: #555;
color: var(--mc-btn-light-active-color);
background-color: var(--mc-btn-light-active-bg);
background-image: none;
border-color: #4d4d4d;
border-color: var(--mc-btn-light-active-border);
}
.btn-check:checked+.btn-light:focus, .btn-check:active+.btn-light:focus, .btn-light:active:focus, .btn-light.active:focus, .show>.btn-light.dropdown-toggle:focus,
.btn-check:focus+.btn-light, .btn-light:focus {
@@ -412,6 +442,26 @@ button[aria-expanded='true'] > .caret {
text-decoration: none;
}
/* role marker on login pages, header icons differ too (color blindness) */
.login-card::before {
content: "";
display: block;
height: 5px;
background: var(--mc-login-color);
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.login-user {
--mc-login-color: var(--mc-login-user);
}
.login-domainadmin {
--mc-login-color: var(--mc-login-domainadmin);
}
.login-admin {
--mc-login-color: var(--mc-login-admin);
}
.hr-title {
display: flex;
align-items: center;
@@ -423,7 +473,7 @@ button[aria-expanded='true'] > .caret {
.hr-title::after {
content: "";
flex: 1;
border-bottom: 1px solid #ccc;
border-bottom: 1px solid var(--mc-hr);
}
.hr-title:not(:empty)::before {
File diff suppressed because it is too large. Load diff
+155 -114
View File
@@ -1,62 +1,103 @@
/* theme tokens, override in css/themes/<name>/dark.css */
:root {
--mc-body-bg: #1c1c1e;
--mc-body-color: #f2f2f7;
--mc-surface: #2c2c2e;
--mc-surface-alt: #282828;
--mc-text: #ccc;
--mc-text-soft: #bbb;
--mc-muted: #8e8e93;
--mc-border: #555;
--mc-tab-border: #444444;
--mc-link: #6fc7e9;
--mc-link-hover: #3daedb;
--mc-btn-bg: #5e5e5e;
--mc-btn-border: #4c4c4e;
--mc-input-bg: #424242;
--mc-input-color: #e2e2e2;
--mc-input-border: #999;
--mc-dropdown-bg: #424242;
--mc-table-bg: #3a3a3a;
--mc-table-stripe: #282828;
--mc-table-stripe-row: #424242;
--mc-table-hover: #343434;
--mc-table-border: #5c5c5c;
--mc-progress-bg: #555;
--mc-placeholder: #d4d4d4;
--mc-btn-outline-color: #e0e0e0;
--mc-btn-outline-border: #7a7a7a;
--mc-btn-outline-hover-bg: #5c5c5c;
--mc-btn-outline-active-bg: #7a7a7a;
--mc-btn-light-active-color: #f2f2f7;
--mc-btn-light-active-bg: #242424;
--mc-btn-light-active-border: #1c1c1e;
--mc-btn-dark-bg: #242424;
--mc-input-disabled-bg: #6e6e6e;
--mc-input-disabled-color: #a8a8a8;
--mc-list-header-bg: #444;
--mc-list-details-bg: #555;
--mc-chip-bg: #444;
--mc-table-child-bg: #414141;
--mc-dtr-bg: #7a7a7a;
--mc-dtr-border: #5c5c5c;
--mc-dtr-color: #e0e0e0;
--mc-dtr-hover-bg: #7a7a7a;
--mc-dtr-active-bg: #949494;
}
body {
background-color: #1c1c1e;
color: #f2f2f7;
background-color: var(--mc-body-bg);
color: var(--mc-body-color);
}
.card {
border: 1px solid #2c2c2e;
background-color: #2c2c2e;
border: 1px solid var(--mc-surface);
background-color: var(--mc-surface);
}
legend {
color: #f2f2f7;
color: var(--mc-body-color);
}
.card-header {
color: #8e8e93;
background-color: #1c1c1e;
color: var(--mc-muted);
background-color: var(--mc-body-bg);
border-color: transparent;
}
.card-body {
--bs-card-color: #bbb;
--bs-card-color: var(--mc-text-soft);
}
.btn-secondary, .paginate_button, .page-link, .btn-light {
color: #f2f2f7 !important;
background-color: #5e5e5e !important;
border-color: #4c4c4e !important;
color: var(--mc-body-color) !important;
background-color: var(--mc-btn-bg) !important;
border-color: var(--mc-btn-border) !important;
}
.btn-dark {
color: #f2f2f7 !important;
background-color: #242424 !important;
border-color: #1c1c1e !important;
}
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
background-color: #444444;
border-color: #4c4c4e !important;
color: #f2f2f7;
color: var(--mc-body-color) !important;
background-color: var(--mc-btn-dark-bg) !important;
border-color: var(--mc-body-bg) !important;
}
.btn-check:checked+.btn-secondary, .btn-check:active+.btn-secondary, .btn-secondary:active, .btn-secondary.active, .show>.btn-secondary.dropdown-toggle {
border-color: #5e5e5e !important;
border-color: var(--mc-btn-bg) !important;
}
.alert-secondary {
color: #f2f2f7 !important;
background-color: #5e5e5e !important;
border-color: #4c4c4e !important;
color: var(--mc-body-color) !important;
background-color: var(--mc-btn-bg) !important;
border-color: var(--mc-btn-border) !important;
}
.bg-secondary {
color: #f2f2f7 !important;
background-color: #5e5e5e !important;
color: var(--mc-body-color) !important;
background-color: var(--mc-btn-bg) !important;
}
.alert-secondary, .alert-secondary a, .alert-secondary .alert-link {
color: #f2f2f7;
color: var(--mc-body-color);
}
.page-item.active .page-link {
@@ -65,18 +106,18 @@ legend {
}
.btn-secondary:focus, .btn-secondary:hover, .btn-group.open .dropdown-toggle.btn-secondary {
background-color: #5e5e5e;
border-color: #4c4c4e !important;
color: #f2f2f7;
background-color: var(--mc-btn-bg);
border-color: var(--mc-btn-border) !important;
color: var(--mc-body-color);
}
.btn-secondary:disabled, .btn-secondary.disabled {
border-color: #5e5e5e !important;
border-color: var(--mc-btn-bg) !important;
}
.modal-content {
--bs-modal-color: #bbb;
background-color: #2c2c2e;
--bs-modal-color: var(--mc-text-soft);
background-color: var(--mc-surface);
}
.modal-header {
@@ -84,7 +125,7 @@ legend {
}
.modal-title {
color: #bbb;
color: var(--mc-text-soft);
}
.modal .btn-close {
@@ -92,12 +133,12 @@ legend {
}
.navbar.bg-light {
background-color: #1c1c1e !important;
border-color: #2c2c2e;
background-color: var(--mc-body-bg) !important;
border-color: var(--mc-surface);
}
.nav-link {
color: #8e8e93 !important;
color: var(--mc-muted) !important;
}
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link {
@@ -105,23 +146,23 @@ legend {
}
.nav-tabs, .nav-tabs .nav-link {
border-color: #444444 !important;
border-color: var(--mc-tab-border) !important;
}
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus, .nav-tabs .nav-link.active {
border-bottom-color: #1c1c1e !important;
border-bottom-color: var(--mc-body-bg) !important;
}
.card .nav-tabs .nav-link:not(.disabled):hover, .card .nav-tabs .nav-link:not(.disabled):focus, .card .nav-tabs .nav-link.active {
border-bottom-color: #2c2c2e !important;
border-bottom-color: var(--mc-surface) !important;
}
.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
color: #f2f2f7 !important;
color: var(--mc-body-color) !important;
}
.dropdown-menu {
background-color: #424242;
background-color: var(--mc-dropdown-bg);
border: 1px solid #282828;
}
.dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover {
@@ -132,79 +173,79 @@ legend {
color: #fff;
}
.bootstrap-select>.dropdown-toggle.bs-placeholder, .bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary {
color: #d4d4d4 !important;
color: var(--mc-placeholder) !important;
}
tbody tr {
color: #ccc;
color: var(--mc-text);
}
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:focus, .navbar-default .navbar-nav>.open>a:hover {
color: #ccc;
color: var(--mc-text);
}
.navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:focus, .navbar-default .navbar-nav>.active>a:hover {
color: #ccc;
color: var(--mc-text);
}
.list-group-item {
background-color: #282828;
border: 1px solid #555;
background-color: var(--mc-surface-alt);
border: 1px solid var(--mc-border);
}
.table-striped>tbody>tr:nth-of-type(odd) {
background-color: #424242;
background-color: var(--mc-table-stripe-row);
}
table.dataTable>tbody>tr.child ul.dtr-details>li {
border-bottom: 1px solid rgba(255, 255, 255, 0.13);
}
.label.label-last-login {
color: #ccc !important;
color: var(--mc-text) !important;
background-color: #555 !important;
}
.progress {
background-color: #555;
background-color: var(--mc-progress-bg);
}
.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
color: #ccc;
color: var(--mc-text);
}
div.numberedtextarea-number {
color: #999;
}
.well {
border: 1px solid #555;
background-color: #282828;
border: 1px solid var(--mc-border);
background-color: var(--mc-surface-alt);
}
pre {
color: #ccc;
background-color: #282828;
border: 1px solid #555;
color: var(--mc-text);
background-color: var(--mc-surface-alt);
border: 1px solid var(--mc-border);
}
.form-control {
background-color: transparent;
}
input.form-control, textarea.form-control {
color: #e2e2e2 !important;
background-color: #424242 !important;
border: 1px solid #999;
color: var(--mc-input-color) !important;
background-color: var(--mc-input-bg) !important;
border: 1px solid var(--mc-input-border);
}
input.form-control:focus, textarea.form-control {
background-color: #424242 !important;
background-color: var(--mc-input-bg) !important;
}
input.form-control:disabled, textarea.form-disabled {
color: #a8a8a8 !important;
background-color: #6e6e6e !important;
color: var(--mc-input-disabled-color) !important;
background-color: var(--mc-input-disabled-bg) !important;
}
.input-group-addon {
color: #ccc;
background-color: #424242 !important;
border: 1px solid #999;
color: var(--mc-text);
background-color: var(--mc-input-bg) !important;
border: 1px solid var(--mc-input-border);
}
.input-group-text {
color: #ccc;
color: var(--mc-text);
background-color: #1c1c1c;
}
.list-group-item {
color: #ccc;
color: var(--mc-text);
}
.dropdown-item {
color: #ccc;
color: var(--mc-text);
}
.dropdown-item:hover {
color: #616161 !important;
@@ -214,9 +255,9 @@ input.form-control:disabled, textarea.form-disabled {
background-color: #007aff;
}
.form-select {
color: #e2e2e2!important;
background-color: #424242!important;
border: 1px solid #999;
color: var(--mc-input-color)!important;
background-color: var(--mc-input-bg)!important;
border: 1px solid var(--mc-input-border);
}
.responsive-tabs .card-header button[data-bs-toggle="collapse"] {
@@ -240,15 +281,15 @@ input.form-control:disabled, textarea.form-disabled {
a {
color: #6fc7e9;
color: var(--mc-link);
text-decoration: underline;
}
a:hover {
color: #3daedb;
color: var(--mc-link-hover);
}
.breadcrumb-item.active {
color: #ccc;
color: var(--mc-text);
}
@@ -273,11 +314,11 @@ a:hover {
background-color: #3a3a3a;
}
.accordion-button {
color: #bbb;
color: var(--mc-text-soft);
background-color: #2c2c2c;
}
.accordion-button:not(.collapsed) {
color: #6fc7e9;
color: var(--mc-link);
background-color: #2c2c2c;
}
.accordion-button::after {
@@ -300,15 +341,15 @@ a:hover {
}
.tag-box {
background-color: #282828;
border: 1px solid #555;
background-color: var(--mc-surface-alt);
border: 1px solid var(--mc-border);
}
.tag-input {
color: #fff;
background-color: #282828;
background-color: var(--mc-surface-alt);
}
.tag-add {
color: #ccc;
color: var(--mc-text);
}
.tag-add:hover {
color: #d1d1d1;
@@ -318,19 +359,19 @@ a:hover {
color: #fff;
}
.btn-outline-secondary:hover {
background-color: #5c5c5c;
background-color: var(--mc-btn-outline-hover-bg);
}
.btn.btn-outline-secondary {
color: #e0e0e0 !important;
border-color: #7a7a7a !important;
color: var(--mc-btn-outline-color) !important;
border-color: var(--mc-btn-outline-border) !important;
}
.btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show {
background-color: #7a7a7a !important;
background-color: var(--mc-btn-outline-active-bg) !important;
}
.btn-check:checked+.btn-light, .btn-check:active+.btn-light, .btn-light:active, .btn-light.active, .show>.btn-light.dropdown-toggle {
color: #f2f2f7 !important;
background-color: #242424 !important;
border-color: #1c1c1e !important;
color: var(--mc-btn-light-active-color) !important;
background-color: var(--mc-btn-light-active-bg) !important;
border-color: var(--mc-btn-light-active-border) !important;
}
.btn-input-missing,
.btn-input-missing:hover,
@@ -348,16 +389,16 @@ a:hover {
}
.list-group-details {
background: #555;
background: var(--mc-list-details-bg);
}
.list-group-header {
background: #444;
background: var(--mc-list-header-bg);
}
span.mail-address-item {
background-color: #444;
background-color: var(--mc-chip-bg);
border-radius: 4px;
border: 1px solid #555;
border: 1px solid var(--mc-border);
padding: 2px 7px;
display: inline-block;
margin: 2px 6px 2px 0;
@@ -365,52 +406,52 @@ span.mail-address-item {
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover {
background-color: #7a7a7a !important;
background-color: var(--mc-dtr-hover-bg) !important;
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before {
background-color: #7a7a7a !important;
border: 1.5px solid #5c5c5c !important;
color: #e0e0e0 !important;
background-color: var(--mc-dtr-bg) !important;
border: 1.5px solid var(--mc-dtr-border) !important;
color: var(--mc-dtr-color) !important;
}
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before,
table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before {
background-color: #949494;
background-color: var(--mc-dtr-active-bg);
}
table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,
table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty {
background-color: #414141;
background-color: var(--mc-table-child-bg);
}
table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
color: #ccc !important;
color: var(--mc-text) !important;
}
.table-secondary {
--bs-table-bg: #282828;
--bs-table-striped-bg: #343434;
--bs-table-striped-color: #f2f2f7;
--bs-table-striped-color: var(--mc-body-color);
--bs-table-active-bg: #4c4c4c;
--bs-table-active-color: #f2f2f7;
--bs-table-active-color: var(--mc-body-color);
--bs-table-hover-bg: #3a3a3a;
--bs-table-hover-color: #f2f2f7;
color: #ccc;
--bs-table-hover-color: var(--mc-body-color);
color: var(--mc-text);
border-color: #3a3a3a;
}
.table-light {
--bs-table-bg: #3a3a3a;
--bs-table-striped-bg: #444444;
--bs-table-striped-color: #f2f2f7;
--bs-table-striped-color: var(--mc-body-color);
--bs-table-active-bg: #5c5c5c;
--bs-table-active-color: #f2f2f7;
--bs-table-active-color: var(--mc-body-color);
--bs-table-hover-bg: #4c4c4c;
--bs-table-hover-color: #f2f2f7;
color: #ccc;
--bs-table-hover-color: var(--mc-body-color);
color: var(--mc-text);
border-color: #4c4c4c;
}
@@ -430,23 +471,23 @@ table.table, .table-striped>tbody>tr:nth-of-type(odd)>*, tbody tr {
.table-striped>tbody>tr:nth-of-type(odd)>td,
.table-striped>tbody>tr:nth-of-type(odd)>th {
background-color: #282828;
background-color: var(--mc-table-stripe);
}
.table-hover>tbody>tr:hover {
background-color: #343434;
background-color: var(--mc-table-hover);
}
.table>:not(caption)>*>* {
border-color: #5c5c5c;
--bs-table-color-state:#bbb;
--bs-table-bg: #3a3a3a;
border-color: var(--mc-table-border);
--bs-table-color-state:var(--mc-text-soft);
--bs-table-bg: var(--mc-table-bg);
}
.text-muted {
--bs-secondary-color: #8e8e93;
--bs-secondary-color: var(--mc-muted);
}
input::placeholder {
color: #8e8e93 !important;
color: var(--mc-muted) !important;
}
.form-select {
+212
View File
@@ -0,0 +1,212 @@
/* servercow theme, dark mode, bundled after mailcow core dark mode */
:root {
--sc-brown-hover: #806b55;
--sc-brown-active: #8a745c;
--sc-brown-soft: rgba(201, 180, 154, .18);
--sc-brown-tint: rgba(201, 180, 154, .08);
--sc-link: #c9b49a;
--sc-danger-text: #ff8a8a;
--sc-ink: #ece7e1;
--sc-muted: #a89f94;
--sc-placeholder: #8a8177;
--sc-bg: #171513;
--sc-surface: #211e1b;
--sc-cap: #2a2622;
--sc-addon: #2a2622;
--sc-line: #463f38;
--sc-divider: #34302b;
--sc-cap-line: #3d3732;
--sc-stripe: rgba(255, 255, 255, .025);
--sc-hover: rgba(201, 180, 154, .08);
--sc-focus-border: #8a7864;
--sc-focus: rgba(201, 180, 154, .25);
--sc-shadow: 0 0 10px 0 rgba(0, 0, 0, .5);
--bs-body-color: #ece7e1;
--bs-body-bg: #171513;
/* core dark tokens, see css/themes/mailcow-darkmode.css */
--mc-body-bg: var(--sc-bg);
--mc-body-color: var(--sc-ink);
--mc-surface: var(--sc-surface);
--mc-surface-alt: var(--sc-cap);
--mc-text: var(--sc-ink);
--mc-text-soft: var(--sc-ink);
--mc-muted: var(--sc-muted);
--mc-border: var(--sc-divider);
--mc-tab-border: var(--sc-divider);
--mc-link: var(--sc-link);
--mc-link-hover: #e0cfba;
--mc-btn-bg: var(--sc-cap);
--mc-btn-border: var(--sc-line);
--mc-input-bg: var(--sc-bg);
--mc-input-color: var(--sc-ink);
--mc-input-border: var(--sc-line);
--mc-dropdown-bg: var(--sc-surface);
--mc-table-bg: transparent;
--mc-table-stripe: var(--sc-stripe);
--mc-table-stripe-row: transparent;
--mc-table-hover: var(--sc-hover);
--mc-table-border: var(--sc-divider);
--mc-progress-bg: var(--sc-cap);
--mc-placeholder: var(--sc-placeholder);
--mc-hr: var(--sc-divider);
--mc-btn-outline-color: var(--sc-ink);
--mc-btn-outline-border: var(--sc-line);
--mc-btn-outline-hover-bg: var(--sc-brown-tint);
--mc-btn-outline-active-bg: var(--sc-brown);
--mc-btn-light-active-color: #fff;
--mc-btn-light-active-bg: var(--sc-brown);
--mc-btn-light-active-border: var(--sc-brown);
--mc-btn-dark-bg: var(--sc-brown);
--mc-input-disabled-bg: var(--sc-cap);
--mc-input-disabled-color: var(--sc-muted);
--mc-list-header-bg: var(--sc-cap);
--mc-list-details-bg: var(--sc-surface);
--mc-chip-bg: var(--sc-cap);
--mc-table-child-bg: var(--sc-surface);
--mc-dtr-bg: var(--sc-cap);
--mc-dtr-border: var(--sc-line);
--mc-dtr-color: var(--sc-link);
--mc-dtr-hover-bg: var(--sc-hover);
--mc-dtr-active-bg: var(--sc-brown-soft);
}
/* navbar */
.navbar.bg-light {
background-color: var(--sc-surface) !important;
border-bottom-color: var(--sc-green);
}
.navbar-light .navbar-nav .nav-link {
color: var(--sc-ink) !important;
}
.navbar-light .navbar-nav .nav-link:hover,
.navbar-light .navbar-nav .nav-link:focus,
.navbar-light .navbar-nav .nav-link.active,
.navbar-light .navbar-nav .show > .nav-link {
color: var(--sc-link) !important;
}
.dropdown-menu {
border-color: var(--sc-cap-line);
}
.dropdown-item:hover, .dropdown-item:focus {
color: var(--sc-link) !important;
}
/* cards and modals */
.card-header {
color: var(--sc-ink);
background-color: var(--sc-cap);
border-color: var(--sc-cap-line);
}
.modal-content {
background-color: var(--sc-surface);
}
.modal-header {
border-color: var(--sc-cap-line);
}
.input-group-text {
background-color: var(--sc-addon);
}
.form-control::file-selector-button {
color: var(--sc-ink);
background-color: var(--sc-cap);
border-color: var(--sc-line);
}
.form-control:hover:not(:disabled):not([readonly])::file-selector-button {
color: var(--sc-link);
background-color: var(--sc-line);
}
.form-check-input:not(:checked) {
background-color: var(--sc-bg);
border-color: var(--sc-line);
}
kbd {
color: var(--sc-ink);
background-color: var(--sc-cap);
}
/* buttons */
.btn-secondary:hover, .btn-secondary:focus, .btn-light:hover, .btn-light:focus, .page-link:hover {
color: var(--sc-link) !important;
background-color: var(--sc-line) !important;
}
.btn-check:checked + .btn-secondary, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle,
.page-item.active .page-link {
color: #fff !important;
background-color: var(--sc-brown) !important;
border-color: var(--sc-brown) !important;
}
/* tabs */
.nav-tabs .nav-link {
color: var(--sc-ink) !important;
border-bottom-color: transparent !important;
}
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus,
.card .nav-tabs .nav-link:not(.disabled):hover, .card .nav-tabs .nav-link:not(.disabled):focus {
color: var(--sc-link) !important;
border-bottom-color: transparent !important;
}
.nav-tabs .nav-link.active, .card .nav-tabs .nav-link.active {
color: var(--sc-link) !important;
border-bottom-color: var(--sc-link) !important;
}
/* semantic text */
.text-success {
color: var(--sc-green) !important;
}
.text-warning {
color: var(--sc-warning) !important;
}
/* alerts, opaque dark tints (also used for floating notifications) */
.alert-primary {
color: #e6d9c9 !important;
background-color: #3c342b !important;
border-color: #5e4f3e !important;
}
.alert-info {
color: #cfe9f6 !important;
background-color: #2f3334 !important;
border-color: #44535a !important;
}
.alert-success {
color: #cdeea0;
background-color: #2e3419;
border-color: #405515;
}
.alert-danger {
color: #f9c4c4;
background-color: #402a28;
border-color: #693a38;
}
.alert-warning {
color: #f5d496;
background-color: #3c2f19;
border-color: #654815;
}
.alert-secondary, .alert-light {
color: var(--sc-ink) !important;
background-color: var(--sc-cap) !important;
border-color: var(--sc-divider) !important;
}
.alert .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
/* slider */
.noUi-target {
background: var(--sc-bg);
border-color: var(--sc-line);
box-shadow: none;
}
.noUi-handle {
background: var(--sc-cap);
border-color: var(--sc-line);
box-shadow: none;
}
.noUi-handle::before, .noUi-handle::after {
background: var(--sc-line);
}
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -0,0 +1,93 @@
Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+584
View File
@@ -0,0 +1,584 @@
/* servercow theme for mailcow, based on the servercow customer portal */
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local(''),
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff2') format('woff2'),
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff') format('woff');
}
@font-face {
font-family: 'Poppins';
font-style: italic;
font-weight: 400;
font-display: swap;
src: local(''),
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff2') format('woff2'),
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff') format('woff');
}
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
font-display: swap;
src: local(''),
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff2') format('woff2'),
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff') format('woff');
}
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 600;
font-display: swap;
src: local(''),
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff2') format('woff2'),
url('/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff') format('woff');
}
@font-face {
font-family: 'Roboto Slab';
font-style: normal;
font-weight: 400;
font-display: swap;
src: local(''),
url('/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff2') format('woff2'),
url('/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff') format('woff');
}
@font-face {
font-family: 'Roboto Slab';
font-style: normal;
font-weight: 500;
font-display: swap;
src: local(''),
url('/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff2') format('woff2'),
url('/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff') format('woff');
}
/* tokens */
:root {
--sc-brown: #6F5D49;
--sc-brown-hover: #5e4f3e;
--sc-brown-active: #594a3a;
--sc-brown-soft: rgba(111, 93, 73, .2);
--sc-brown-tint: rgba(111, 93, 73, .08);
--sc-link: #6F5D49;
--sc-green: #8AD608;
--sc-blue: #96D0EC;
--sc-warning: #e3950b;
--sc-danger: #ef6f6f;
--sc-danger-hover: #e85e5e;
--sc-danger-text: #c43c3c;
--sc-ink: #212529;
--sc-muted: #6c757d;
--sc-placeholder: #6c757d;
--sc-bg: #F6F8F8;
--sc-surface: #ffffff;
--sc-cap: #f7f7f7;
--sc-addon: #e9ecef;
--sc-line: #ced4da;
--sc-divider: #dee2e6;
--sc-cap-line: rgba(0, 0, 0, .175);
--sc-stripe: rgba(111, 93, 73, .04);
--sc-hover: rgba(111, 93, 73, .08);
--sc-focus-border: #b7aea4;
--sc-focus: rgba(111, 93, 73, .25);
--sc-shadow: 0 0 10px 0 rgba(134, 134, 134, .3);
--sc-font: 'Poppins', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--sc-font-head: 'Roboto Slab', Georgia, serif;
--bs-body-color: #212529;
--bs-body-bg: #F6F8F8;
--bs-body-font-family: var(--sc-font);
/* core tokens, see css/build/014-mailcow.css */
--mc-font-family: var(--sc-font);
--mc-font-size: 10pt;
--mc-body-bg: var(--sc-bg);
--mc-primary: var(--sc-brown);
--mc-primary-border: var(--sc-brown);
--mc-info: var(--sc-blue);
--mc-info-border: var(--sc-blue);
--mc-border: var(--sc-divider);
--mc-surface: var(--sc-surface);
--mc-surface-alt: var(--sc-cap);
--mc-muted: var(--sc-muted);
--mc-hr: var(--sc-divider);
--mc-progress-bg: var(--sc-addon);
--mc-placeholder: var(--sc-placeholder);
--mc-btn-outline-color: var(--sc-ink);
--mc-btn-outline-border: var(--sc-line);
--mc-btn-outline-hover-bg: var(--sc-brown-tint);
--mc-btn-outline-active-bg: var(--sc-brown);
--mc-btn-light-active-color: #fff;
--mc-btn-light-active-bg: var(--sc-brown);
--mc-btn-light-active-border: var(--sc-brown);
--mc-dtr-bg: var(--sc-surface);
--mc-dtr-border: var(--sc-line);
--mc-dtr-color: var(--sc-brown);
--mc-dtr-hover-bg: var(--sc-brown-tint);
--mc-dtr-active-bg: var(--sc-brown-soft);
--mc-login-user: var(--sc-green);
--mc-login-domainadmin: #f5c400;
--mc-login-admin: var(--sc-danger);
}
/* base */
body {
color: var(--sc-ink);
-webkit-font-smoothing: antialiased;
}
h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6, legend, .modal-title {
font-family: var(--sc-font-head);
font-weight: 500;
color: var(--sc-ink);
}
a {
color: var(--sc-link);
}
a:hover {
color: var(--sc-brown-hover);
}
.text-muted, .text-secondary {
color: var(--sc-muted) !important;
}
::selection {
background: var(--sc-brown-soft);
}
hr {
border-color: var(--sc-divider);
opacity: 1;
}
::placeholder {
color: var(--sc-placeholder) !important;
}
/* navbar, white with green line */
.navbar.bg-light {
background-color: var(--sc-surface) !important;
border: 0;
border-bottom: 5px solid var(--sc-green);
}
.navbar-light .navbar-nav .nav-link {
color: var(--sc-ink);
}
.navbar-light .navbar-nav .nav-link:hover,
.navbar-light .navbar-nav .nav-link:focus,
.navbar-light .navbar-nav .nav-link.active,
.navbar-light .navbar-nav .show > .nav-link {
color: var(--sc-brown);
}
@media (min-width: 992px) {
.navbar-nav .nav-link {
height: 60px;
}
}
.dropdown-menu {
border: 1px solid var(--sc-cap-line);
border-radius: 0;
box-shadow: var(--sc-shadow);
}
.dropdown-item {
color: var(--sc-ink);
}
.dropdown-item:hover, .dropdown-item:focus {
color: var(--sc-brown) !important;
background-color: var(--sc-brown-soft);
}
.dropdown-item.active, .dropdown-item:active {
color: #fff;
background-color: var(--sc-brown);
}
/* cards, white with soft shadow */
.card {
border: 0 !important;
border-radius: 0;
background-color: var(--sc-surface);
box-shadow: var(--sc-shadow);
}
.card-header, .card-header:first-child {
font-family: var(--sc-font-head);
font-weight: 500;
font-size: 1.1rem;
color: var(--sc-ink);
background-color: var(--sc-cap);
border-bottom: 1px solid var(--sc-cap-line);
border-radius: 0;
padding: .5rem 1rem;
}
.card-header .btn, .card-header .form-control, .card-header .form-select {
font-family: var(--sc-font);
font-size: .875rem;
}
.card-footer {
background-color: var(--sc-cap);
border-top: 1px solid var(--sc-cap-line);
border-radius: 0;
}
.card .card {
box-shadow: none;
border: 1px solid var(--sc-divider) !important;
}
/* colored headers become a top line */
.card:has(> .card-header.bg-danger) {
border-top: 4px solid var(--sc-danger) !important;
}
.card-header.bg-danger, .card-header.bg-success, .card-header.bg-primary, .card-header.bg-info {
color: var(--sc-ink) !important;
background-color: var(--sc-cap) !important;
}
.list-group-item {
border-color: var(--sc-divider);
}
/* modals */
.modal-content {
border: 0;
border-radius: 0;
box-shadow: 0 8px 40px rgba(0, 0, 0, .2);
}
.modal-header {
background-color: var(--sc-cap);
border-bottom: 1px solid var(--sc-cap-line);
border-radius: 0;
}
.modal-footer {
border-top: 1px solid var(--sc-divider);
}
/* buttons, flat and square */
.btn {
border-width: 1px;
border-radius: 0;
font-weight: 400;
transition: none;
}
.btn:not(.disabled):hover,
.btn:not(.disabled):active {
margin-top: 0;
border-bottom-width: 1px;
}
.btn:focus, .btn-check:focus + .btn {
box-shadow: 0 0 0 .2rem var(--sc-focus);
}
.btn-primary, .btn-primary:hover, .btn-primary:focus, .btn-primary:active {
color: #fff;
}
.btn-primary:hover, .btn-primary:focus, .btn-primary:active {
background-color: var(--sc-brown-hover) !important;
border-color: var(--sc-brown-active) !important;
}
/* main call to action, portal primary */
.btn-success, .btn-success:not(.disabled):hover, .btn-success:not(.disabled):active, .btn-success:focus,
.btn-success:disabled, .btn-success.disabled {
color: #fff;
background-color: var(--sc-brown);
border-color: var(--sc-brown);
}
.btn-success:hover, .btn-success:active, .btn-success:focus {
background-color: var(--sc-brown-hover);
border-color: var(--sc-brown-active);
}
.btn-danger, .btn-danger:not(.disabled):hover, .btn-danger:not(.disabled):active, .btn-danger:focus,
.btn-danger:disabled, .btn-danger.disabled {
color: #000;
background-color: var(--sc-danger);
border-color: var(--sc-danger);
}
.btn-danger:hover, .btn-danger:active, .btn-danger:focus {
background-color: var(--sc-danger-hover);
border-color: var(--sc-danger-hover);
}
.btn-warning, .btn-warning:not(.disabled):hover, .btn-warning:not(.disabled):active, .btn-warning:focus {
color: #000;
background-color: var(--sc-warning);
border-color: var(--sc-warning);
}
.bg-info, .alert-info, .btn-info, .badge.bg-info > a {
color: #000 !important;
}
.btn-secondary, .btn-secondary:not(.disabled):hover, .btn-secondary:not(.disabled):active, .btn-secondary:focus,
.btn-light, .btn-light:not(.disabled):hover, .btn-light:not(.disabled):active, .btn-light:focus {
color: var(--sc-ink);
background-color: var(--sc-surface);
border-color: var(--sc-line);
}
.btn-secondary:hover, .btn-secondary:active, .btn-secondary:focus,
.btn-light:hover, .btn-light:focus {
color: var(--sc-brown);
background-color: var(--sc-brown-tint);
border-color: var(--sc-line);
}
.btn-check:checked + .btn-secondary, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle {
color: #fff;
background-color: var(--sc-brown);
border-color: var(--sc-brown);
}
.btn-check:checked + .btn-outline-secondary, .btn-outline-secondary.active {
color: #fff !important;
border-color: var(--sc-brown) !important;
}
.btn-outline-primary {
color: var(--sc-brown);
border-color: var(--sc-brown);
}
.btn-outline-primary:hover {
color: #fff;
background-color: var(--sc-brown);
border-color: var(--sc-brown);
}
/* active toggle, e.g. tagging options */
.btn-dark, .btn-dark:hover, .btn-dark:focus, .btn-dark:active {
color: #fff;
background-color: var(--sc-brown);
border-color: var(--sc-brown);
}
.btn-xs, .btn-group-xs > .btn {
border-radius: 0;
}
/* forms */
.form-control, .form-select {
border-color: var(--sc-line);
border-radius: 0;
box-shadow: none;
color: var(--sc-ink);
background-color: var(--sc-surface);
}
.form-control:focus, .form-select:focus {
border-color: var(--sc-focus-border);
box-shadow: 0 0 0 .25rem var(--sc-focus);
}
.form-control::file-selector-button {
color: var(--sc-ink);
background-color: var(--sc-addon);
border-color: var(--sc-line);
}
.form-control:hover:not(:disabled):not([readonly])::file-selector-button {
color: var(--sc-brown);
background-color: var(--sc-brown-soft);
}
.input-group-text {
color: var(--sc-ink);
background-color: var(--sc-addon);
border-color: var(--sc-line);
border-radius: 0;
}
.input-group > :not(:first-child), .input-group > :not(:last-child) {
border-radius: 0;
}
.form-check-input {
border-color: var(--sc-line);
}
.form-check-input:checked {
background-color: var(--sc-brown);
border-color: var(--sc-brown);
}
.form-check-input:focus {
border-color: var(--sc-focus-border);
box-shadow: 0 0 0 .25rem var(--sc-focus);
}
.bootstrap-select > .dropdown-toggle {
border-radius: 0;
}
.bootstrap-select > .dropdown-toggle.bs-placeholder,
.bootstrap-select > .dropdown-toggle.bs-placeholder:hover,
.bootstrap-select > .dropdown-toggle.bs-placeholder:focus,
.bootstrap-select > .dropdown-toggle.bs-placeholder:active {
color: var(--mc-placeholder);
}
.bootstrap-select .dropdown-menu li a.active, .bootstrap-select .dropdown-menu li a:active {
color: #fff;
background-color: var(--sc-brown);
}
div.numberedtextarea-number {
color: var(--sc-muted);
}
/* nav tabs */
.nav-tabs {
border-bottom: 1px solid var(--sc-divider);
}
.nav-tabs .nav-link,
.nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus {
margin-top: 0;
color: var(--sc-ink);
border: 0;
border-bottom: 3px solid transparent;
border-radius: 0;
background: none;
}
.nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus {
padding-bottom: .5rem;
color: var(--sc-brown);
}
.nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link,
.nav-tabs .nav-link:not(.disabled).active {
padding-bottom: .5rem;
color: var(--sc-brown);
background: none;
border-bottom: 3px solid var(--sc-brown);
}
.nav-pills .nav-link {
border-radius: 0;
color: var(--sc-ink);
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
background-color: var(--sc-brown);
}
/* tables */
.table {
--bs-table-bg: transparent;
--bs-table-striped-bg: var(--sc-stripe);
--bs-table-striped-color: var(--sc-ink);
--bs-table-hover-color: var(--sc-ink);
--bs-table-hover-bg: var(--sc-hover);
color: var(--sc-ink);
border-color: var(--sc-divider);
}
.table > thead > tr > th, table.dataTable thead th {
color: var(--sc-ink);
font-weight: 600;
border-bottom: 1px solid var(--sc-line);
}
table.dataTable.table-striped > tbody > tr.odd > * {
box-shadow: inset 0 0 0 9999px var(--sc-stripe);
}
table.dataTable.table-hover > tbody > tr:hover > * {
box-shadow: inset 0 0 0 9999px var(--sc-hover);
}
.table > :not(:first-child) {
border-top: 0;
}
.dataTables_wrapper .dataTables_filter input,
.dataTables_wrapper .dataTables_length select {
border: 1px solid var(--sc-line);
border-radius: 0;
}
.pagination > li > a, .pagination > li > span, .pagination .page-link,
.pagination > li > a:hover, .pagination > li > a:focus, .pagination > li > a:active {
top: 0;
color: var(--sc-brown);
font-weight: 400;
text-transform: none;
background-color: var(--sc-surface);
border: 1px solid var(--sc-divider);
border-radius: 0 !important;
}
.pagination > li > a:hover, .pagination > li > a:focus {
background-color: var(--sc-brown-tint);
}
.pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:active,
.page-item.disabled .page-link {
color: var(--sc-muted);
background-color: var(--sc-surface);
border-width: 1px;
border-color: var(--sc-divider);
}
.page-item.active .page-link {
color: #fff;
background-color: var(--sc-brown);
border-color: var(--sc-brown);
}
.breadcrumb {
border: 1px solid var(--sc-divider);
border-radius: 0;
}
/* alerts like the portal */
.alert {
border: 1px solid;
border-radius: 0;
}
.alert-primary {
color: #43382c !important;
background-color: #e2dfdb !important;
border-color: #d4cec8 !important;
}
.alert-info {
color: #3c535e !important;
background-color: #eaf6fb !important;
border-color: #e0f1f9 !important;
}
.alert-success {
color: #375603;
background-color: #e8f7ce;
border-color: #dcf3b5;
}
.alert-danger {
color: #8f4343;
background-color: #fce2e2;
border-color: #fad4d4;
}
.alert-warning {
color: #885907;
background-color: #f9eace;
border-color: #f7dfb6;
}
.alert-secondary, .alert-light {
color: var(--sc-ink);
background-color: var(--sc-cap);
border-color: var(--sc-divider);
}
.alert .alert-link {
color: inherit;
}
.alert .btn-close {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414z'/%3e%3c/svg%3e");
opacity: .5;
}
/* badges and semantic colors, readable text on top */
.badge {
font-weight: 500;
border-radius: 0;
}
.bg-success {
color: #000 !important;
background-color: var(--sc-green) !important;
}
.bg-warning {
color: #000 !important;
background-color: var(--sc-warning) !important;
}
.bg-danger {
color: #000 !important;
background-color: var(--sc-danger) !important;
}
.badge.bg-secondary, .badge.bg-light {
color: var(--sc-ink);
background-color: var(--sc-addon) !important;
}
.text-success {
color: #4f8a0b !important;
}
.text-danger {
color: var(--sc-danger-text) !important;
}
.text-warning {
color: #9a6100 !important;
}
.progress {
border-radius: 0;
}
.progress-bar {
color: #000;
background-color: var(--sc-green);
}
.progress-bar.bg-danger, .progress-bar-danger {
color: #000;
background-color: var(--sc-danger) !important;
}
.progress-bar-warning {
color: #000;
background-color: var(--sc-warning) !important;
}
/* footer */
.footer, .footer a {
color: var(--sc-muted);
}
.footer hr {
border-color: var(--sc-divider);
}
+4
View File
@@ -0,0 +1,4 @@
{
"name": "servercow",
"description": "Based on the servercow theme"
}
+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'));
+67
View File
@@ -0,0 +1,67 @@
<?php
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
header('Content-Type: application/json');
// Admins only
if (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] !== 'admin') {
http_response_code(403);
echo json_encode(array('status' => 'error', 'message' => 'access denied'));
exit;
}
$owner = $GLOBALS['MAILCOW_GIT_OWNER'];
$repo = $GLOBALS['MAILCOW_GIT_REPO'];
$current = $GLOBALS['MAILCOW_GIT_VERSION'];
// Cache key is bound to the running version, so an update invalidates it naturally
$cache_key = 'MAILCOW_UPDATE_CHECK/' . $current;
// Serve cached result if present (one GitHub call per TTL, not one per page load)
$cached = $redis->get($cache_key);
if ($cached !== false) {
echo $cached;
exit;
}
// GitHub requires a User-Agent and rate-limits unauthenticated requests to 60/h per IP
function github_get($url) {
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => 'mailcow-update-check',
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => array('Accept: application/vnd.github+json'),
));
$body = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($body === false || $code !== 200) return false;
$json = json_decode($body, true);
return is_array($json) ? $json : false;
}
$latest = github_get("https://api.github.com/repos/{$owner}/{$repo}/releases/latest");
$release = github_get("https://api.github.com/repos/{$owner}/{$repo}/releases/tags/{$current}");
// Any failure (rate limit, offline, unexpected payload) -> honest error, cached briefly
if ($latest === false || $release === false ||
empty($latest['tag_name']) || empty($latest['created_at']) || empty($release['created_at'])) {
$result = json_encode(array('status' => 'error', 'message' => 'could not reach GitHub API'));
$redis->setex($cache_key, 300, $result);
echo $result;
exit;
}
$date_latest = strtotime($latest['created_at']);
$date_current = strtotime($release['created_at']);
if ($date_latest <= $date_current) {
$result = json_encode(array('status' => 'no_update'));
} else {
$result = json_encode(array('status' => 'update_available', 'latest_tag' => $latest['tag_name']));
}
// Cache the positive result for an hour
$redis->setex($cache_key, 3600, $result);
echo $result;
+68
View File
@@ -233,6 +233,38 @@ function customize($_action, $_item, $_data = null) {
'msg' => 'custom_login_modified'
);
break;
case 'ui_theme':
$ui_theme = is_array($_data['ui_theme']) ? (string)reset($_data['ui_theme']) : (string)$_data['ui_theme'];
if ($ui_theme !== '' && !array_key_exists($ui_theme, customize('get', 'ui_themes'))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'ui_theme_invalid'
);
return false;
}
try {
if ($ui_theme === '') {
$redis->del('UI_THEME');
}
else {
$redis->set('UI_THEME', $ui_theme);
}
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => 'ui_theme_modified'
);
break;
}
break;
case 'delete':
@@ -402,6 +434,42 @@ function customize($_action, $_item, $_data = null) {
return false;
}
break;
case 'ui_themes':
// theme folders in css/themes/<name>/ with a theme.css
$themes = array();
foreach (glob('/web/css/themes/*/theme.css') as $theme_css) {
$theme_dir = dirname($theme_css);
$theme = basename($theme_dir);
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $theme)) {
continue;
}
$meta = json_decode(@file_get_contents($theme_dir . '/theme.json'), true);
$themes[$theme] = array(
'name' => (!empty($meta['name'])) ? $meta['name'] : $theme,
'description' => (!empty($meta['description'])) ? $meta['description'] : '',
'dark' => file_exists($theme_dir . '/dark.css')
);
}
ksort($themes);
return $themes;
break;
case 'ui_theme':
try {
$ui_theme = $redis->get('UI_THEME');
}
catch (RedisException $e) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_item, $_data),
'msg' => array('redis_error', $e)
);
return false;
}
if (empty($ui_theme) || !preg_match('/^[a-zA-Z0-9_-]+$/', $ui_theme) || !file_exists('/web/css/themes/' . $ui_theme . '/theme.css')) {
return '';
}
return $ui_theme;
break;
}
break;
}
+14 -3
View File
@@ -429,14 +429,25 @@ function domain_admin_sso($_action, $_data) {
switch ($_action) {
case 'check':
$token = $_data;
$token = preg_replace('/[^a-zA-Z0-9-]/', '', $_data);
$stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;");
$stmt->execute(array(
':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token)
':token' => $token
));
$return = $stmt->fetch(PDO::FETCH_ASSOC);
return empty($return['username']) ? false : $return['username'];
if (empty($return['username'])) {
return false;
}
// single-use: consume the token; if a concurrent request already used it, deny
$del = $pdo->prepare("DELETE FROM `da_sso` WHERE `token` = :token");
$del->execute(array(
':token' => $token
));
if ($del->rowCount() < 1) {
return false;
}
return $return['username'];
case 'issue':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
+83 -14
View File
@@ -53,6 +53,39 @@ function valid_network($network) {
function valid_hostname($hostname) {
return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME);
}
// Validates a browser-style Origin: scheme://host[:port], no path/query/fragment/credentials
function valid_origin($origin) {
$parts = parse_url($origin);
if ($parts === false || !isset($parts['scheme']) || !isset($parts['host'])) {
return false;
}
if (!in_array(strtolower($parts['scheme']), array('http', 'https'), true)) {
return false;
}
if (isset($parts['user']) || isset($parts['pass']) || isset($parts['path']) || isset($parts['query']) || isset($parts['fragment'])) {
return false;
}
$host = $parts['host'];
$host_without_brackets = trim($host, '[]');
if (!valid_hostname($host_without_brackets) && !filter_var($host_without_brackets, FILTER_VALIDATE_IP)) {
return false;
}
// Reject anything that doesn't round-trip to the exact same origin (e.g. stray trailing slash)
$port = isset($parts['port']) ? ':' . $parts['port'] : '';
$rebuilt = strtolower($parts['scheme']) . '://' . strtolower($host) . $port;
return strtolower($origin) === $rebuilt;
}
// Bring an origin into the exact form a browser sends it (scheme://host[:port], lowercase).
function normalize_cors_origin($origin) {
$origin = trim($origin);
if ($origin === '*') {
return '*';
}
if ($origin !== '' && !preg_match('~^[a-z][a-z0-9+.-]*://~i', $origin)) {
$origin = 'https://' . $origin;
}
return valid_origin($origin) ? strtolower($origin) : false;
}
// Thanks to https://stackoverflow.com/a/49373789
// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6
function ip_acl($ip, $networks) {
@@ -558,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'])) {
@@ -600,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)) {
@@ -2283,10 +2328,13 @@ function cors($action, $data = null) {
return false;
}
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array($_SERVER['SERVER_NAME']);
$allowed_origins = isset($data['allowed_origins']) ? $data['allowed_origins'] : array(getBaseURL());
$allowed_origins = !is_array($allowed_origins) ? array_filter(array_map('trim', explode("\n", $allowed_origins))) : $allowed_origins;
foreach ($allowed_origins as $origin) {
if (!filter_var($origin, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) && $origin != '*') {
foreach ($allowed_origins as &$origin) {
if ($origin === '*') {
continue;
}
if (!valid_origin($origin)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $action, $data),
@@ -2294,7 +2342,10 @@ function cors($action, $data = null) {
);
return false;
}
// browsers always send a lowercase scheme/host in the Origin header, so normalize to match
$origin = strtolower($origin);
}
unset($origin);
$allowed_methods = isset($data['allowed_methods']) ? $data['allowed_methods'] : array('GET', 'POST', 'PUT', 'DELETE');
$allowed_methods = !is_array($allowed_methods) ? array_map('trim', preg_split( "/( |,|;|\n)/", $allowed_methods)) : $allowed_methods;
@@ -2342,26 +2393,35 @@ function cors($action, $data = null) {
);
}
$cors_settings = !$cors_settings ? array('allowed_origins' => $_SERVER['SERVER_NAME'], 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? $_SERVER['SERVER_NAME'] : $cors_settings['allowed_origins'];
$cors_settings = !$cors_settings ? array('allowed_origins' => getBaseURL(), 'allowed_methods' => 'GET, POST, PUT, DELETE') : $cors_settings;
$cors_settings['allowed_origins'] = empty($cors_settings['allowed_origins']) ? getBaseURL() : $cors_settings['allowed_origins'];
$cors_settings['allowed_methods'] = empty($cors_settings['allowed_methods']) ? 'GET, POST, PUT, DELETE, OPTION' : $cors_settings['allowed_methods'];
return $cors_settings;
break;
case "set_headers":
$cors_settings = cors('get');
// normalize the stored list; it may still hold bare hostnames written before origins were validated as origins
$allowed_origins = array_filter(array_map('normalize_cors_origin', explode(',', $cors_settings['allowed_origins'])));
$origin = isset($_SERVER['HTTP_ORIGIN']) ? normalize_cors_origin($_SERVER['HTTP_ORIGIN']) : false;
// check if requested origin is in allowed origins
$allowed_origins = explode(', ', $cors_settings['allowed_origins']);
$cors_settings['allowed_origins'] = $allowed_origins[0];
if (in_array('*', $allowed_origins)){
$cors_settings['allowed_origins'] = '*';
} else if (array_key_exists('HTTP_ORIGIN', $_SERVER) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) {
$cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN'];
$allow_origin = null;
if (in_array('*', $allowed_origins, true)) {
$allow_origin = '*';
} else if ($origin !== false && in_array($origin, $allowed_origins, true)) {
$allow_origin = $origin;
}
// always allow OPTIONS for preflight request
$cors_settings["allowed_methods"] = empty($cors_settings["allowed_methods"]) ? 'OPTIONS' : $cors_settings["allowed_methods"] . ', ' . 'OPTIONS';
header('Access-Control-Allow-Origin: ' . $cors_settings['allowed_origins']);
// a disallowed origin gets no Access-Control-Allow-Origin at all; echoing a different origin than the requesting one tells a browser nothing
if ($allow_origin !== null) {
header('Access-Control-Allow-Origin: ' . $allow_origin);
}
if ($allow_origin !== '*') {
// the response depends on the request origin, keep caches from mixing them up
header('Vary: Origin', false);
}
header('Access-Control-Allow-Methods: '. $cors_settings['allowed_methods']);
header('Access-Control-Allow-Headers: Accept, Content-Type, X-Api-Key, Origin');
@@ -3503,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();
}
+56 -11
View File
@@ -41,13 +41,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
else {
$username = $_SESSION['mailcow_cc_username'];
}
if (isset($_data["validity"]) && !filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'validity_missing'
);
return false;
if (isset($_data["validity"])) {
if (!filter_var($_data["validity"], FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 87600)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'validity_missing'
);
return false;
}
}
else {
// Default to 1 yr
@@ -60,7 +62,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$permanent = 0;
}
$domain = $_data['domain'];
$description = $_data['description'];
// spamalias.description is NOT NULL, an absent description must not become a null insert
$description = $_data['description'] ?? '';
$valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain'];
$valid_alias_domains = user_get_alias_details($username)['alias_domains'];
if (!empty($valid_alias_domains)) {
@@ -747,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");
@@ -2712,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',
@@ -5320,6 +5348,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$mailboxdata['rl_scope'] = 'domain';
}
$mailboxdata['is_relayed'] = $row['backupmx'];
// Internal send-as ACL for this mailbox, mirrors the sender_acl field accepted by edit/mailbox
$mailboxdata['sender_acl'] = array();
$stmt = $pdo->prepare("SELECT `send_as` FROM `sender_acl` WHERE `logged_in_as` = :username AND `external` = '0'");
$stmt->execute(array(':username' => $_data));
$sender_acl_rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($sender_acl_rows as $sender_acl_row) {
$mailboxdata['sender_acl'][] = $sender_acl_row['send_as'];
}
}
$stmt = $pdo->prepare("SELECT `tag_name`
FROM `tags_mailbox` WHERE `username`= :username");
@@ -5647,8 +5683,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $username . "/%' OR `c_uid` = :username");
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
$c_object_like = '%/' . addcslashes($username, '\\%_') . '/%';
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
$stmt->execute(array(
':c_object_like' => $c_object_like,
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
@@ -6044,8 +6083,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . str_replace('%', '\%', $username) . "/%' OR `c_uid` = :username");
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
$c_object_like = '%/' . addcslashes($username, '\\%_') . '/%';
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
$stmt->execute(array(
':c_object_like' => $c_object_like,
':username' => $username
));
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
@@ -6201,8 +6243,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$stmt->execute(array(
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE '%/" . $name . "/%' OR `c_uid` = :username");
// bind LIKE pattern + escape LIKE wildcards so the value matches literally (no SQLi, no over-match)
$c_object_like = '%/' . addcslashes($name, '\\%_') . '/%';
$stmt = $pdo->prepare("DELETE FROM `sogo_acl` WHERE `c_object` LIKE :c_object_like OR `c_uid` = :username");
$stmt->execute(array(
':c_object_like' => $c_object_like,
':username' => $name
));
$stmt = $pdo->prepare("DELETE FROM `sogo_store` WHERE `c_folder_id` IN (SELECT `c_folder_id` FROM `sogo_folder_info` WHERE `c_path2` = :username)");
+12 -8
View File
@@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) {
return false;
}
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
WHERE `qhash` = :hash
WHERE `qhash` = :hash
AND user_acl.quarantine = 1
AND rcpt IN (SELECT username FROM mailbox)');
$stmt->execute(array(':hash' => $hash));
@@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) {
return false;
}
$stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt`
WHERE `qhash` = :hash
WHERE `qhash` = :hash
AND `user_acl`.`quarantine` = 1
AND `username` IN (SELECT `username` FROM `mailbox`)');
$stmt->execute(array(':hash' => $hash));
@@ -170,6 +170,8 @@ function quarantine($_action, $_data = null) {
}
elseif ($release_format == 'raw') {
$detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']);
// dot-stuffing per RFC5321 4.5.2: escape leading dots
$detail_row['msg'] = preg_replace('~^\.~m', '..', $detail_row['msg']);
$postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)),
@@ -180,7 +182,7 @@ function quarantine($_action, $_data = null) {
array('221', '')
);
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
if (!$smtp_connection) {
logger(array('return' => array(
array(
@@ -192,7 +194,7 @@ function quarantine($_action, $_data = null) {
return false;
}
for ($i=0; $i < count($postfix_talk); $i++) {
$smtp_resource = fgets($smtp_connection, 256);
$smtp_resource = fgets($smtp_connection, 256);
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
$ret = substr($smtp_resource, 0, 3);
$ret = (empty($ret)) ? '-' : $ret;
@@ -465,17 +467,19 @@ function quarantine($_action, $_data = null) {
}
elseif ($release_format == 'raw') {
$row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']);
// dot-stuffing per RFC5321 4.5.2: escape leading dots
$row['msg'] = preg_replace('~^\.~m', '..', $row['msg']);
$postfix_talk = array(
array('220', 'HELO quarantine' . chr(10)),
array('250', 'MAIL FROM: ' . $sender . chr(10)),
array('250', 'RCPT TO: ' . $row['rcpt'] . chr(10)),
array('250', 'DATA' . chr(10)),
array('354', str_replace("\n.", '', $row['msg']) . chr(10) . '.' . chr(10)),
array('354', $row['msg'] . chr(10) . '.' . chr(10)),
array('250', 'QUIT' . chr(10)),
array('221', '')
);
// Thanks to https://stackoverflow.com/questions/6632399/given-an-email-as-raw-text-how-can-i-send-it-using-php
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
$smtp_connection = fsockopen($postfix, 590, $errno, $errstr, 1);
if (!$smtp_connection) {
$_SESSION['return'][] = array(
'type' => 'warning',
@@ -485,7 +489,7 @@ function quarantine($_action, $_data = null) {
return false;
}
for ($i=0; $i < count($postfix_talk); $i++) {
$smtp_resource = fgets($smtp_connection, 256);
$smtp_resource = fgets($smtp_connection, 256);
if (substr($smtp_resource, 0, 3) !== $postfix_talk[$i][0]) {
$ret = substr($smtp_resource, 0, 3);
$ret = (empty($ret)) ? '-' : $ret;
@@ -833,7 +837,7 @@ function quarantine($_action, $_data = null) {
)));
return false;
}
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
$stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash');
$stmt->execute(array(':hash' => $hash));
return $stmt->fetch(PDO::FETCH_ASSOC);
break;
+15
View File
@@ -22,6 +22,9 @@ if (preg_match("/debug/i", $_SERVER['REQUEST_URI'])) {
if ($_SERVER['REQUEST_URI'] == '/') {
$css_minifier->add('/web/css/site/index.css');
}
if (file_exists('/web/css/themes/'.$UI_THEME.'/theme.css')) {
$css_minifier->add('/web/css/themes/'.$UI_THEME.'/theme.css');
}
$hash = $css_minifier->getDataHash();
$CSSPath = '/tmp/' . $hash . '.css';
@@ -30,6 +33,17 @@ if(!file_exists($CSSPath)) {
cleanupCSS($hash);
}
// dark mode bundle, core dark mode + theme dark.css
$dark_minifier = new CSSminifierExtended('/web/css/themes/mailcow-darkmode.css');
if (file_exists('/web/css/themes/'.$UI_THEME.'/dark.css')) {
$dark_minifier->add('/web/css/themes/'.$UI_THEME.'/dark.css');
}
$DarkCSSPath = '/tmp/' . $hash . '-' . $dark_minifier->getDataHash() . '.css';
if(!file_exists($DarkCSSPath)) {
$dark_minifier->minify($DarkCSSPath);
}
$dark_css_path = '/cache/' . basename($DarkCSSPath);
$mailcow_apps_processed = $MAILCOW_APPS;
$app_links = customize('get', 'app_links');
$app_links_processed = $app_links;
@@ -77,6 +91,7 @@ $globalVariables = [
'dual_login' => @$_SESSION['dual-login'],
'ui_texts' => $UI_TEXTS,
'css_path' => '/cache/'.basename($CSSPath),
'dark_css_path' => $dark_css_path,
'logo' => customize('get', 'main_logo'),
'logo_dark' => customize('get', 'main_logo_dark'),
'available_languages' => $AVAILABLE_LANGUAGES,
+5 -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(
@@ -1464,6 +1465,8 @@ function init_db_schema()
"pop3_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['pop3_access']),
"smtp_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['smtp_access']),
"sieve_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['sieve_access']),
"eas_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['eas_access']),
"dav_access" => intval($GLOBALS['MAILBOX_DEFAULT_ATTRIBUTES']['dav_access']),
"acl_spam_alias" => 1,
"acl_tls_policy" => 1,
"acl_spam_score" => 1,
+7 -1
View File
@@ -319,10 +319,16 @@ $css_minifier = new CSSminifierExtended();
$css_dir = array_diff(scandir('/web/css/build'), array('..', '.'));
// get customized ui data
$UI_TEXTS = customize('get', 'ui_texts');
// theme selected in admin UI overrides $UI_THEME
if ($ui_theme_selected = customize('get', 'ui_theme')) {
$UI_THEME = $ui_theme_selected;
}
// minify bootstrap theme
if (file_exists('/web/css/themes/'.$UI_THEME.'-bootstrap.css'))
if (file_exists('/web/css/themes/'.$UI_THEME.'/bootstrap.css'))
$css_minifier->add('/web/css/themes/'.$UI_THEME.'/bootstrap.css');
elseif (file_exists('/web/css/themes/'.$UI_THEME.'-bootstrap.css'))
$css_minifier->add('/web/css/themes/'.$UI_THEME.'-bootstrap.css');
else
$css_minifier->add('/web/css/themes/lumen-bootstrap.css');
+6
View File
@@ -121,6 +121,8 @@ $AVAILABLE_LANGUAGES = array(
// default theme is lumen
// additional themes can be found here: https://bootswatch.com/
// copy them to data/web/css/themes/{THEME-NAME}-bootstrap.css
// or use a theme folder data/web/css/themes/{THEME-NAME}/ (theme.css, optional dark.css, bootstrap.css, theme.json)
// theme folders can be selected in System > Configuration > Customize, which overrides $UI_THEME
$UI_THEME = "lumen";
// Show DKIM private keys - false by default
@@ -246,6 +248,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
+3 -3
View File
@@ -1,7 +1,7 @@
/*!
* Bootstrap v5.1.3 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Bootstrap v5.3.8 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=(t,e,i)=>{Object.keys(i).forEach((n=>{const s=i[n],r=e[n],a=r&&o(r)?"element":null==(l=r)?`${l}`:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}))},l=t=>!(!o(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",(()=>{p.forEach((t=>t()))})),p.push(e)):e()},_=t=>{"function"==typeof t&&t()},b=(e,i,n=!0)=>{if(!n)return void _(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),_(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},v=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,E=/::\d+$/,A={};let T=1;const O={mouseenter:"mouseover",mouseleave:"mouseout"},C=/^(mouseenter|mouseleave)/i,k=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function x(t){const e=L(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function D(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;s<o;s++){const o=t[n[s]];if(o.originalHandler===e&&o.delegationSelector===i)return o}return null}function S(t,e,i){const n="string"==typeof e,s=n?i:e;let o=P(t);return k.has(o)||(o=t),[n,s,o]}function N(t,e,i,n,s){if("string"!=typeof e||!t)return;if(i||(i=n,n=null),C.test(e)){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=S(e,i,n),l=x(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=L(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&j.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&j.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function I(t,e,i,n,s){const o=D(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function P(t){return t=t.replace(w,""),O[t]||t}const j={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!Line truncated
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,function(){"use strict";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i="transitionend",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,(t,e)=>`#${CSS.escape(e)}`)),t),s=t=>null==t?`${t}`:Object.prototype.toString.call(t).match(/\s([a-z]+)/i)[1].toLowerCase(),o=t=>{t.dispatchEvent(new Event(i))},r=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),a=t=>r(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(n(t)):null,l=t=>{if(!r(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",()=>{for(const t of p)t()}),p.push(e)):e()},_=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,b=(t,e,n=!0)=>{if(!n)return void _(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),_(t))};e.addEventListener(i,a),setTimeout(()=>{r||o(e)},s)},v=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,A=/::\d+$/,E={};let T=1;const C={mouseenter:"mouseover",mouseleave:"mouseout"},O=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function x(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function k(t){const e=x(t);return t.uidEvent=e,E[e]=E[e]||{},E[e]}function L(t,e,i=null){return Object.values(t).find(t=>t.callable===e&&t.delegationSelector===i)}function S(t,e,i){const n="string"==typeof e,s=n?i:e||i;let o=N(t);return O.has(o)||(o=t),[n,s,o]}function D(t,e,i,n,s){if("string"!=typeof e||!t)return;let[o,r,a]=S(e,i,n);if(e in C){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=k(t),c=l[a]||(l[a]={}),h=L(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=x(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return j(s,{delegateTarget:r}),n.oneOff&&P.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return j(n,{delegateTarget:t}),i.oneOff&&P.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function $(t,e,i,n,s){const o=L(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function I(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&$(t,e,i,r.callable,r.delegationSelector)}function N(t){return t=t.replace(w,""),C[t]||t}const P={on(t,e,i,n){D(t,e,i,n,!1)},one(t,e,i,n){D(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=k(t),c=l[rLine truncated
//# sourceMappingURL=bootstrap.bundle.min.js.map
+4 -1
View File
@@ -124,6 +124,9 @@ $(document).ready(function() {
var lastTab = localStorage.getItem(key);
if (lastTab) {
$('[data-bs-target="#' + lastTab + '"]').click();
var dropdown = $('[data-bs-target="#' + lastTab + '"]').closest('.dropdown');
dropdown.find('.dropdown-menu').removeClass('show');
dropdown.find('.dropdown-toggle').attr('aria-expanded', 'false');
var tab = $('[id^="' + lastTab + '"]');
$(tab).find('.card-body.collapse:first').collapse('show');
}
@@ -347,7 +350,7 @@ $(document).ready(function() {
if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_dark.png');
localStorage.setItem('mailcow_theme', 'light');
}else{
$('head').append('<link id="dark-mode-theme" rel="stylesheet" type="text/css" href="/css/themes/mailcow-darkmode.css">');
$('head').append('<link id="dark-mode-theme" rel="stylesheet" type="text/css" href="' + mailcow_dark_css + '">');
$('#dark-mode-toggle').prop('checked', true);
$('.main-logo').addClass('d-none');
$('.main-logo-dark').removeClass('d-none');
+27 -33
View File
@@ -30,7 +30,7 @@ $(document).ready(function() {
// create host cpu and mem charts
createHostCpuAndMemChart();
// check for new version
if (mailcow_info.branch === "master"){
if (mailcow_info.branch === "master" && mailcow_cc_role === "admin"){
check_update(mailcow_info.version_tag, mailcow_info.project_url);
}
$("#mailcow_version").click(function(){
@@ -1089,6 +1089,8 @@ jQuery(function($){
return str;
}).join('<br>\n');
item.subject = escapeHtml(item.subject);
if (item.sender_mime != null) item.sender_mime = escapeHtml(item.sender_mime);
if (item['message-id'] != null) item['message-id'] = escapeHtml(item['message-id']);
var scan_time = item.time_real.toFixed(3);
if (item.time_virtual) {
scan_time += ' / ' + item.time_virtual.toFixed(3);
@@ -1167,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>';
});
@@ -1174,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') {
@@ -1212,6 +1216,11 @@ jQuery(function($){
});
} else if (table == 'rllog') {
$.each(data, function (i, item) {
if (item.header_from != null) item.header_from = escapeHtml(item.header_from);
if (item.header_subject != null) item.header_subject = escapeHtml(item.header_subject);
if (item.from != null) item.from = escapeHtml(item.from);
if (item.rcpt != null) item.rcpt = escapeHtml(item.rcpt);
if (item.message_id != null) item.message_id = escapeHtml(item.message_id);
if (item.user == null) {
item.user = "none";
}
@@ -1669,41 +1678,26 @@ function createHostCpuAndMemChart(){
function check_update(current_version, github_repo_url){
if (!current_version || !github_repo_url) return false;
var github_account = github_repo_url.split("/")[3];
var github_repo_name = github_repo_url.split("/")[4];
// get details about latest release
window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/latest", {method:'GET',cache:'no-cache'}).then(function(response) {
window.fetch("/inc/ajax/update_check.php", {method:'GET',cache:'no-cache'}).then(function(response) {
if (!response.ok) throw new Error("update check returned " + response.status);
return response.json();
}).then(function(latest_data) {
// get details about current release
window.fetch("https://api.github.com/repos/"+github_account+"/"+github_repo_name+"/releases/tags/"+current_version, {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
}).then(function(current_data) {
// compare releases
var date_current = new Date(current_data.created_at);
var date_latest = new Date(latest_data.created_at);
if (date_latest.getTime() <= date_current.getTime()){
// no update available
$("#mailcow_update").removeClass("text-warning text-danger").addClass("text-success");
$("#mailcow_update").html("<b>" + lang_debug.no_update_available + "</b>");
} else {
// update available
$("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning");
$("#mailcow_update").html(lang_debug.update_available + ` <a href="#" id="mailcow_update_changelog">`+latest_data.tag_name+`</a>`);
$("#mailcow_update_changelog").click(function(){
if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin")
return;
}).then(function(data) {
if (data.status === "no_update") {
$("#mailcow_update").removeClass("text-warning text-danger").addClass("text-success");
$("#mailcow_update").html("<b>" + lang_debug.no_update_available + "</b>");
} else if (data.status === "update_available") {
$("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning");
$("#mailcow_update").html(lang_debug.update_available + ` <a href="#" id="mailcow_update_changelog">`+data.latest_tag+`</a>`);
$("#mailcow_update_changelog").click(function(){
if (mailcow_cc_role !== "admin" && mailcow_cc_role !== "domainadmin")
return;
showVersionModal("New Release " + latest_data.tag_name, latest_data.tag_name);
})
}
}).catch(err => {
// err
console.log(err);
$("#mailcow_update").removeClass("text-success text-warning").addClass("text-danger");
$("#mailcow_update").html("<b>"+ lang_debug.update_failed +"</b>");
});
showVersionModal("New Release " + data.latest_tag, data.latest_tag);
})
} else {
throw new Error(data.message || "update check failed");
}
}).catch(err => {
// err
console.log(err);
+1
View File
@@ -996,6 +996,7 @@ jQuery(function($){
'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '</div></div>'
};
item.username = escapeHtml(item.username);
item.name = escapeHtml(item.name);
if (Array.isArray(item.tags)){
var tags = '';
+3
View File
@@ -2038,6 +2038,9 @@ if (isset($_GET['query'])) {
case "custom_login":
process_edit_return(customize('edit', 'custom_login', $attr));
break;
case "ui_theme":
process_edit_return(customize('edit', 'ui_theme', $attr));
break;
case "self":
if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
process_edit_return(domain_admin('edit', $attr));
+10
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)",
@@ -394,6 +395,11 @@
"ui_header_announcement_type_info": "Info",
"ui_header_announcement_type_warning": "Wichtig",
"ui_texts": "UI-Label und Texte",
"ui_theme": "Theme",
"ui_theme_dark": "Enthält eine Darkmode-Variante",
"ui_theme_default": "Standard",
"ui_theme_info": "Themes sind Ordner in <code>data/web/css/themes/&lt;name&gt;/</code> mit folgenden Dateien:<ul class=\"mb-0\"><li><code>theme.css</code> (erforderlich)</li><li><code>dark.css</code> (optional, Darkmode)</li><li><code>bootstrap.css</code> (optional, ersetzt das Bootstrap-Basistheme)</li><li><code>theme.json</code> (optional, <code>name</code> und <code>description</code>)</li></ul>",
"ui_theme_none": "Keine Themes gefunden.",
"unban_pending": "ausstehend",
"unchanged_if_empty": "Unverändert, wenn leer",
"upload": "Hochladen",
@@ -443,6 +449,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",
@@ -548,6 +555,7 @@
"webauthn_authenticator_failed": "Der ausgewählte Authenticator wurde nicht gefunden",
"webauthn_publickey_failed": "Zu dem ausgewählten Authenticator wurde kein Publickey hinterlegt",
"webauthn_username_failed": "Der ausgewählte Authenticator gehört zu einem anderen Konto",
"ui_theme_invalid": "Theme nicht gefunden",
"unknown": "Ein unbekannter Fehler trat auf",
"unknown_tfa_method": "Unbekannte TFA-Methode",
"unlimited_quota_acl": "Unendliche Quota untersagt durch ACL",
@@ -728,6 +736,7 @@
"mta_sts_mx": "MX-Server",
"mta_sts_mx_info": "Erlaubt das Senden nur an explizit aufgeführte Mailserver-Hostnamen; der sendende MTA überprüft, ob der DNS-MX-Hostname mit der Richtlinienliste übereinstimmt, und erlaubt die Zustellung nur mit einem gültigen TLS-Zertifikat (schützt vor MITM).",
"mta_sts_mx_notice": "Es können mehrere MX-Server angegeben werden (durch Kommas getrennt).",
"mta_sts_active_info": "Wenn nicht angehakt, wird die MTA-STS-Richtlinie nicht veröffentlicht und für die mta-sts-Subdomain kein Zertifikat über ACME angefordert.",
"multiple_bookings": "Mehrfaches Buchen",
"nexthop": "Next Hop",
"none_inherit": "Keine Auswahl / Erben",
@@ -1203,6 +1212,7 @@
"tls_policy_map_entry_deleted": "TLS-Richtlinie mit der ID %s wurde gelöscht",
"tls_policy_map_entry_saved": "TLS-Richtlinieneintrag \"%s\" wurde gespeichert",
"ui_texts": "Änderungen an UI-Texten",
"ui_theme_modified": "Theme wurde erfolgreich gespeichert",
"upload_success": "Datei wurde erfolgreich hochgeladen",
"verified_fido2_login": "FIDO2-Anmeldung verifiziert",
"verified_totp_login": "TOTP-Anmeldung verifiziert",
+10
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",
@@ -404,6 +405,11 @@
"ui_header_announcement_type_info": "Info",
"ui_header_announcement_type_warning": "Important",
"ui_texts": "UI labels and texts",
"ui_theme": "Theme",
"ui_theme_dark": "Includes a dark mode variant",
"ui_theme_default": "Default",
"ui_theme_info": "Themes are folders in <code>data/web/css/themes/&lt;name&gt;/</code> with the following files:<ul class=\"mb-0\"><li><code>theme.css</code> (required)</li><li><code>dark.css</code> (optional, dark mode)</li><li><code>bootstrap.css</code> (optional, replaces the Bootstrap base theme)</li><li><code>theme.json</code> (optional, <code>name</code> and <code>description</code>)</li></ul>",
"ui_theme_none": "No themes found.",
"unban_pending": "unban pending",
"unchanged_if_empty": "If unchanged leave blank",
"upload": "Upload",
@@ -444,6 +450,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",
@@ -552,6 +559,7 @@
"webauthn_authenticator_failed": "The selected authenticator was not found",
"webauthn_publickey_failed": "No public key was stored for the selected authenticator",
"webauthn_username_failed": "The selected authenticator belongs to another account",
"ui_theme_invalid": "Theme not found",
"unknown": "An unknown error occurred",
"unknown_tfa_method": "Unknown TFA method",
"unlimited_quota_acl": "Unlimited quota prohibited by ACL",
@@ -728,6 +736,7 @@
"mta_sts_mx": "MX server",
"mta_sts_mx_info": "Allows sending only to explicitly listed mail server hostnames; the sending MTA checks if the DNS MX hostname matches the policy list, and only allows delivery with a valid TLS certificate (guards against MITM).",
"mta_sts_mx_notice": "Multiple MX servers can be specified (separated by commas).",
"mta_sts_active_info": "If unchecked, the MTA-STS policy will not be published and no certificate will be requested for the mta-sts subdomain via ACME.",
"multiple_bookings": "Multiple bookings",
"none_inherit": "None / Inherit",
"nexthop": "Next hop",
@@ -1211,6 +1220,7 @@
"tls_policy_map_entry_deleted": "TLS policy map ID %s has been deleted",
"tls_policy_map_entry_saved": "TLS policy map entry \"%s\" has been saved",
"ui_texts": "Saved changes to UI texts",
"ui_theme_modified": "Theme was saved successfully",
"upload_success": "File uploaded successfully",
"verified_fido2_login": "Verified FIDO2 login",
"verified_totp_login": "Verified TOTP login",
+5 -2
View File
@@ -109,7 +109,10 @@
"bcc_dest_format": "Il destinatario in copia nascosta deve essere un singolo indirizzo email.<br>Se si vuole spedire una copia del messaggio a più destinatari, bisogna creare un alias ed utilizzarlo per questa opzione.",
"app_passwd_protocols": "Protocolli consentiti per la password dell'app",
"tags": "Tag",
"dry": "Simula sincronizzazione"
"dry": "Simula sincronizzazione",
"internal": "Interno",
"internal_info": "Gli alias interni sono accessibili solo dal proprio dominio o dai domini alias.",
"sender_allowed": "Consenti l'invio con questo alias"
},
"admin": {
"access": "Accedi",
@@ -131,7 +134,7 @@
"additional_rows": " righe aggiuntive inserite",
"admin": "Amministratore",
"admin_details": "Modifica impostazioni amministratore",
"admin_domains": "Assengazioni di dominio",
"admin_domains": "Assegnazioni di dominio",
"admins": "Amministratori",
"admins_ldap": "Amministratori LDAP",
"advanced_settings": "Impostazioni avanzate",
+5 -3
View File
@@ -786,7 +786,8 @@
"mta_sts_mx_info": "Permite envio apenas para nomes de host de servidor de email explicitamente listados; o MTA de envio verifica se o nome do host DNS MX corresponde à lista de políticas e permite entrega apenas com certificado TLS válido (protege contra MITM).",
"mta_sts_mx_notice": "Múltiplos servidores MX podem ser especificados (separados por vírgulas).",
"sender_allowed": "Permitir enviar como este alias",
"sender_allowed_info": "Se desativado, este alias poderá apenas receber e-mails. Use a ACL de remetente para substituir essa configuração e conceder a caixas de correio específicas permissão para enviar."
"sender_allowed_info": "Se desativado, este alias poderá apenas receber e-mails. Use a ACL de remetente para substituir essa configuração e conceder a caixas de correio específicas permissão para enviar.",
"mta_sts_active_info": "Se esta opção estiver desmarcada, a política MTA-STS não será publicada e nenhum certificado será solicitado via ACME para o subdomínio mta-sts."
},
"fido2": {
"confirm": "Confirme",
@@ -1034,7 +1035,8 @@
"weekly": "Semanalmente",
"yes": "✓",
"iam": "Provedor de Identidade",
"internal": "Interno"
"internal": "Interno",
"force_tfa": "A2F"
},
"oauth2": {
"access_denied": "Faça login como proprietário da mailbox para conceder acesso via OAuth2.",
@@ -1086,7 +1088,7 @@
"rspamd_result": "Resultado do Rspamd",
"sender": "Remetente (SMTP)",
"sender_header": "Remetente (cabeçalho “De”)",
"settings_info": "Quantidade máxima de elementos a serem colocados em quarentena: %s <br>Tamanho máximo do e-mail: %s MiB",
"settings_info": "Número máximo de elementos em quarentena (por caixa de correio): %s <br>Tamanho máximo do e-mail: %s MiB",
"show_item": "Mostrar item",
"spam": "Spam",
"spam_score": "Ponto",
+7 -4
View File
@@ -29,7 +29,8 @@
"extend_sender_acl": "Dovoli razširitev pošiljateljevega ACL z zunanjimi e-poštnimi naslovi",
"quarantine_category": "Spremeni kategorijo obvestil o karanteni",
"syncjobs": "Sinhronizacijska opravila",
"pw_reset": "Dovoli ponastavitev uporabniškega gesla mailcow"
"pw_reset": "Dovoli ponastavitev uporabniškega gesla mailcow",
"alias_external_goto": "Dovoli vzdevke z zunanjimi domenami goto"
},
"add": {
"active": "Aktivno",
@@ -559,7 +560,8 @@
"mx_invalid": "Zapis MX %s je neveljaven",
"version_invalid": "Različica %s je neveljavna",
"tfa_removal_blocked": "Dvofaktorske avtentikacije ni mogoče odstraniti, ker je obvezna za vaš račun.",
"quarantine_category_invalid": "Kategorija karantene mora biti ena od: add_header, reject, all"
"quarantine_category_invalid": "Kategorija karantene mora biti ena od: add_header, reject, all",
"external_goto_denied": "Zunanji naslov goto %s ni dovoljen"
},
"debug": {
"containers_info": "Informacije o zabojniku",
@@ -785,7 +787,8 @@
"internal": "Notranje",
"internal_info": "Notranji vzdevki so dostopni samo iz lastne domene ali vzdevkov domen.",
"sender_allowed": "Dovoli pošiljanje kot ta vzdevek",
"sender_allowed_info": "Če je onemogočeno, lahko ta vzdevek samo prejema pošto. Za preglasitev in dodelitev dovoljenja za pošiljanje določenim poštnim predalom uporabite seznam za nadzor dostopa pošiljatelja."
"sender_allowed_info": "Če je onemogočeno, lahko ta vzdevek samo prejema pošto. Za preglasitev in dodelitev dovoljenja za pošiljanje določenim poštnim predalom uporabite seznam za nadzor dostopa pošiljatelja.",
"mta_sts_active_info": "Če ni označeno, pravilnik MTA-STS ne bo objavljen in za poddomeno mta-sts ne bo zahtevano nobeno potrdilo prek ACME."
},
"footer": {
"restart_container_info": "<b>Pomembno:</b> Eleganten ponovni zagon lahko traja nekaj časa, zato počakajte, da se konča.",
@@ -1078,7 +1081,7 @@
"rspamd_result": "Rezultat Rspamd",
"sender": "Pošiljatelj (SMTP)",
"sender_header": "Pošiljatelj (glava »Od«)",
"settings_info": "Največje število elementov za karanteno: %s<br>Največja velikost e-pošte: %s MiB",
"settings_info": "Največje število elementov za karanteno (na poštni predal): %s<br>Največja velikost e-pošte: %s MiB",
"show_item": "Prikaži element",
"spam": "Neželena pošta",
"subj": "Zadeva",
+22 -7
View File
@@ -29,7 +29,8 @@
"syncjobs": "同步任务",
"tls_policy": "TLS 策略",
"unlimited_quota": "无限邮箱容量配额",
"pw_reset": "允许重置mailcow使用者密码"
"pw_reset": "允许重置mailcow使用者密码",
"alias_external_goto": "允许带有外部目标地址的别名"
},
"add": {
"activate_filter_warn": "当“启用”选项被勾选后,其它所有的过滤器都会被禁用。",
@@ -111,7 +112,8 @@
"validation_success": "验证成功",
"dry": "模拟同步(Dry run)",
"internal_info": "内部的别名只能在域内部或者别名域内部访问。",
"internal": "内部的"
"internal": "内部的",
"sender_allowed": "允许以该别名发送"
},
"admin": {
"access": "权限管理",
@@ -555,7 +557,10 @@
"max_age_invalid": "最大有效时间 %s 无效",
"mode_invalid": "模式 %s 无效",
"mx_invalid": "MX 记录 %s 无效",
"version_invalid": "版本 %s 无效"
"version_invalid": "版本 %s 无效",
"tfa_removal_blocked": "无法移除两步验证,你的账户必须使用两步验证。",
"external_goto_denied": "不允许使用外部目标地址 %s",
"quarantine_category_invalid": "隔离分类必须是 add_header, reject, all 的其中一个"
},
"debug": {
"chart_this_server": "图表 (此服务器)",
@@ -754,7 +759,10 @@
"mta_sts_max_age_info": "接收方邮件服务器可缓存该策略的时长(秒),超出后需重新获取策略。",
"mta_sts_mx": "MX 服务器",
"mta_sts_mx_info": "仅允许向明确列出的邮件服务器发送邮件;发送方 MTA 会验证 DNS MX 记录的主机名是否与策略列表匹配,并仅允许携带有效 TLS 证书的投递(可防范中间人攻击)。",
"mta_sts_mx_notice": "可配置多个 MX 服务器(以逗号分隔)。"
"mta_sts_mx_notice": "可配置多个 MX 服务器(以逗号分隔)。",
"sender_allowed": "允许以此别名发送",
"sender_allowed_info": "如果禁用了,此别名只能接收邮件。使用发件者 ACL 来覆盖此选项并赋予指定的邮箱发送权限。",
"mta_sts_active_info": "如果未选中,MTA-STS 策略不会被公布并且不会通过 ACME 为 MTA-STS 子域名请求证书。"
},
"fido2": {
"confirm": "确认",
@@ -1002,7 +1010,8 @@
"templates": "模板",
"template": "模板",
"iam": "身份提供者(IDP)",
"internal": "内部的"
"internal": "内部的",
"force_tfa": "两步验证"
},
"oauth2": {
"access_denied": "请作为邮箱所有者登录以使用 OAuth2 授权。",
@@ -1210,7 +1219,12 @@
"waiting_usb_auth": "<i>等待 USB 设备中...</i><br><br>现在请触碰你的 WebAuthn USB 设备上的按钮。",
"waiting_usb_register": "<i>等待 USB 设备中...</i><br><br>请在上方输入你的密码并请触碰你的 WebAuthn USB 设备上的按钮以确认注册该 WebAuthn 设备。",
"yubi_otp": "Yubico OTP 认证",
"authenticators": "验证器(Authenticators)"
"authenticators": "验证器(Authenticators)",
"force_tfa": "在登录时强制启用 2FA",
"force_tfa_info": "在访问面板前用户会被要求设置两步验证。",
"setup_title": "需要设置两步验证",
"setup_required": "你的账户需要设置两步验证。请添加一个两步验证方法以继续。",
"cancel_setup": "取消并注销"
},
"user": {
"action": "操作",
@@ -1384,7 +1398,8 @@
"overview": "概览",
"expire_never": "永不过期",
"forever": "永久",
"spam_aliases_info": "垃圾邮件别名是一种临时电子邮件地址,可用于保护真实电子邮件地址。<br>还可以选择设置过期时间,以便在设定的时间后自动停用别名,从而有效地销毁被滥用或泄露的地址。"
"spam_aliases_info": "垃圾邮件别名是一种临时电子邮件地址,可用于保护真实电子邮件地址。<br>还可以选择设置过期时间,以便在设定的时间后自动停用别名,从而有效地销毁被滥用或泄露的地址。",
"pw_update_required": "你的账户需要修改一次密码。请设置一个新密码以继续。"
},
"warning": {
"cannot_delete_self": "不能删除已登录的用户",
+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']);
+11 -2
View File
@@ -8,8 +8,12 @@ $ALLOW_ADMIN_EMAIL_LOGIN = (preg_match(
$session_var_user_allowed = 'sogo-sso-user-allowed';
$session_var_pass = 'sogo-sso-pass';
// only the internal nginx auth_request loopback (127.0.0.1:65510) sets this;
// external clients can never supply it (bare fastcgi param, not an HTTP header)
$is_internal_auth = (($_SERVER['SOGO_AUTH_INTERNAL'] ?? '') === '1');
// validate credentials for basic auth requests
if (isset($_SERVER['PHP_AUTH_USER'])) {
if ($is_internal_auth && isset($_SERVER['PHP_AUTH_USER'])) {
// load prerequisites only when required
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
@@ -50,6 +54,11 @@ elseif (isset($_GET['login'])) {
(($_SESSION['acl']['login_as'] == "1" && $ALLOW_ADMIN_EMAIL_LOGIN !== 0) || ($is_dual === false && $login == $_SESSION['mailcow_cc_username']))) {
if (filter_var($login, FILTER_VALIDATE_EMAIL)) {
if (user_get_alias_details($login) !== false) {
// enforce tenant boundary
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $login)) {
header("Location: /");
exit;
}
// Block SOGo access if pending actions (2FA setup, password update)
if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) {
header("Location: /");
@@ -80,7 +89,7 @@ elseif (isset($_GET['login'])) {
exit;
}
// only check for admin-login on sogo GUI requests
elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0) {
elseif ($is_internal_auth && isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HTTP_X_ORIGINAL_URI'], 0, 9), "/SOGo/so/") === 0) {
// this is an nginx auth_request call, we check for existing sogo-sso session variables
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.inc.php';
if (file_exists($_SERVER['DOCUMENT_ROOT'] . '/inc/vars.local.inc.php')) {
@@ -7,7 +7,33 @@
<span class="d-none d-md-block">{{ lang.admin.customize }}</span>
</div>
<div id="collapse-tab-config-customize" class="card-body collapse" data-bs-parent="#admin-content">
<legend><i class="bi bi-file-image"></i> {{ lang.admin.change_logo }}</legend><hr />
<legend><i class="bi bi-palette"></i> {{ lang.admin.ui_theme }}</legend><hr />
<div class="text-muted mb-3">{{ lang.admin.ui_theme_info|raw }}</div>
<form class="form" data-id="ui_theme" role="form" method="post">
<div class="row">
<div class="col-12 col-md-6 col-lg-4">
<p><select data-width="100%" id="ui_theme" name="ui_theme" class="selectpicker show-tick">
<option value="" {% if not ui_theme %}selected{% endif %}>{{ lang.admin.ui_theme_default }}</option>
{% for key, theme in ui_themes %}
<option value="{{ key }}" {% if ui_theme == key %}selected{% endif %}>{{ theme.name }}</option>
{% endfor %}
</select></p>
</div>
</div>
{% if ui_themes %}
<ul class="list-unstyled text-muted small mb-4">
{% for key, theme in ui_themes %}
<li><b>{{ theme.name }}</b> <code>{{ key }}</code>{% if theme.dark %} <i class="bi bi-moon-fill" title="{{ lang.admin.ui_theme_dark }}"></i>{% endif %}{% if theme.description %} - {{ theme.description }}{% endif %}</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted small mb-4">{{ lang.admin.ui_theme_none }}</p>
{% endif %}
<p><div class="btn-group">
<button class="btn btn-sm btn-xs-half d-block d-sm-inline btn-success" data-action="edit_selected" data-item="admin" data-id="ui_theme" data-api-url='edit/ui_theme' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
</div></p>
</form>
<legend style="padding-top:20px"><i class="bi bi-file-image"></i> {{ lang.admin.change_logo }}</legend><hr />
<p class="text-muted">{{ lang.admin.logo_info }}</p>
<form class="form-inline" role="form" method="post" enctype="multipart/form-data">
<div class="mb-4">
+3 -2
View File
@@ -6,9 +6,9 @@
<div class="row mb-4" style="margin-top: 60px">
<div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
<div class="card">
<div class="card login-card login-admin">
<div class="card-header d-flex align-items-center text-break">
<i class="bi bi-person-fill me-2"></i> {{ lang.login.login_admin }}
<i class="bi bi-shield-lock-fill me-2"></i> {{ lang.login.login_admin }}
<div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
<label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
<input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
@@ -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">
+6 -1
View File
@@ -10,6 +10,7 @@
<link rel="stylesheet" href="{{ css_path }}">
<script>
var mailcow_dark_css = '{{ dark_css_path }}';
// check if darkmode is preferred by OS or set by localStorage
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && localStorage.getItem("mailcow_theme") !== "light" ||
localStorage.getItem("mailcow_theme") === "dark") {
@@ -18,7 +19,7 @@
link.id = 'dark-mode-theme';
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = '/css/themes/mailcow-darkmode.css';
link.href = mailcow_dark_css;
head.appendChild(link);
}
</script>
@@ -473,6 +474,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);
}
},
+3 -2
View File
@@ -6,9 +6,9 @@
<div class="row mb-4" style="margin-top: 60px">
<div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
<div class="card">
<div class="card login-card login-domainadmin">
<div class="card-header d-flex align-items-center text-break">
<i class="bi bi-person-fill me-2"></i> {{ lang.login.login_dadmin }}
<i class="bi bi-globe2 me-2"></i> {{ lang.login.login_dadmin }}
<div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
<label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
<input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
@@ -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
@@ -340,7 +340,7 @@
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="active"{% if mta_sts.active == '1' %} checked{% endif %}> {{ lang.edit.active }}</label>
<label><i style="font-size: 16px; cursor: pointer;" class="bi bi-patch-question-fill m-2 ms-0" data-bs-toggle="tooltip" data-bs-html="true" data-bs-placement="bottom" title="{{ lang.edit.mta_sts_active_info|raw }}"></i><input type="checkbox" class="form-check-input" value="1" name="active"{% if mta_sts.active == '1' %} checked{% endif %}> {{ lang.edit.active }}</label>
</div>
</div>
</div>
+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">
+2 -2
View File
@@ -7,12 +7,12 @@
<li class="nav-item dropdown" role="presentation">
<a class="nav-link dropdown-toggle active" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.domains }}</a>
<ul class="dropdown-menu">
<li><button class="dropdown-item" aria-selected="false" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
<li><button class="dropdown-item active" aria-selected="true" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
<li><button class="dropdown-item" aria-selected="false" aria-controls="tab-templates-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-templates-domains">{{ lang.mailbox.templates }}</button></li>
</ul>
</li>
{% else %}
<li class="nav-item" role="presentation"><button class="nav-link" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
<li class="nav-item" role="presentation"><button class="nav-link active" aria-selected="true" aria-controls="tab-domains" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domains">{{ lang.mailbox.domains }}</button></li>
{% endif %}
{% if mailcow_cc_role == 'admin' %}
<li class="nav-item dropdown" role="presentation">
+2 -1
View File
@@ -6,7 +6,7 @@
<div class="row mb-4" style="margin-top: 60px">
<div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
<div class="card">
<div class="card login-card login-user">
<div class="card-header d-flex align-items-center text-break">
<i class="bi bi-person-fill me-2"></i> {{ lang.login.login_user }}
<div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
@@ -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;">
+8 -8
View File
@@ -1,7 +1,7 @@
services:
unbound-mailcow:
image: ghcr.io/mailcow/unbound:1.25.1-1
image: ghcr.io/mailcow/unbound:1.26.1-1
environment:
- TZ=${TZ}
- SKIP_UNBOUND_HEALTHCHECK=${SKIP_UNBOUND_HEALTHCHECK:-n}
@@ -42,7 +42,7 @@ services:
- mysql
redis-mailcow:
image: redis:7.4.6-alpine
image: redis:7.4.11-alpine
entrypoint: ["/bin/sh","/redis-conf.sh"]
volumes:
- redis-vol-1:/data/
@@ -65,7 +65,7 @@ services:
- redis
clamd-mailcow:
image: ghcr.io/mailcow/clamd:1.71
image: ghcr.io/mailcow/clamd:1.4.6-1
restart: always
depends_on:
unbound-mailcow:
@@ -84,7 +84,7 @@ services:
- clamd
rspamd-mailcow:
image: ghcr.io/mailcow/rspamd:4.1.0-1
image: ghcr.io/mailcow/rspamd:4.2.1-1
stop_grace_period: 30s
depends_on:
- dovecot-mailcow
@@ -200,7 +200,7 @@ services:
- phpfpm
sogo-mailcow:
image: ghcr.io/mailcow/sogo:5.12.9-1
image: ghcr.io/mailcow/sogo:5.12.11-1
environment:
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
@@ -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.14.0
depends_on:
unbound-mailcow:
condition: service_healthy
@@ -419,7 +419,7 @@ services:
- php-fpm-mailcow
- sogo-mailcow
- rspamd-mailcow
image: ghcr.io/mailcow/nginx:1.30.3-1
image: ghcr.io/mailcow/nginx:1.30.4-1
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:
@@ -465,7 +465,7 @@ services:
condition: service_started
unbound-mailcow:
condition: service_healthy
image: ghcr.io/mailcow/acme:1.97
image: ghcr.io/mailcow/acme:1.98
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment: