diff --git a/.github/workflows/close_old_issues_and_prs.yml b/.github/workflows/close_old_issues_and_prs.yml index f3306b7df..13dbac279 100644 --- a/.github/workflows/close_old_issues_and_prs.yml +++ b/.github/workflows/close_old_issues_and_prs.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Mark/Close Stale Issues and Pull Requests 🗑️ - uses: actions/stale@v10.1.1 + uses: actions/stale@v11.0.0 with: repo-token: ${{ secrets.STALE_ACTION_PAT }} days-before-stale: 60 diff --git a/.github/workflows/image_builds.yml b/.github/workflows/image_builds.yml index 12182a6b2..aa56b0381 100644 --- a/.github/workflows/image_builds.yml +++ b/.github/workflows/image_builds.yml @@ -15,7 +15,7 @@ jobs: images: - "acme-mailcow" - "clamd-mailcow" - - "controller-mailcow" + - "dockerapi-mailcow" - "dovecot-mailcow" - "netfilter-mailcow" - "olefy-mailcow" @@ -27,7 +27,7 @@ jobs: - "watchdog-mailcow" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Setup Docker run: | curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh diff --git a/.github/workflows/pr_to_nightly.yml b/.github/workflows/pr_to_nightly.yml index 548fabde6..8d6b2524d 100644 --- a/.github/workflows/pr_to_nightly.yml +++ b/.github/workflows/pr_to_nightly.yml @@ -8,11 +8,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - name: Run the Action - uses: devops-infra/action-pull-request@v1.0.2 + uses: devops-infra/action-pull-request@v1.4.0 with: github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }} title: Automatic PR to nightly from ${{ github.event.repository.updated_at}} diff --git a/.github/workflows/rebuild_backup_image.yml b/.github/workflows/rebuild_backup_image.yml index 10edd5cd0..4490afee1 100644 --- a/.github/workflows/rebuild_backup_image.yml +++ b/.github/workflows/rebuild_backup_image.yml @@ -13,24 +13,24 @@ jobs: packages: write steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to GHCR if: github.event_name != 'pull_request' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/update_postscreen_access_list.yml b/.github/workflows/update_postscreen_access_list.yml index 066616aee..ed63281c7 100644 --- a/.github/workflows/update_postscreen_access_list.yml +++ b/.github/workflows/update_postscreen_access_list.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Generate postscreen_access.cidr run: | diff --git a/.gitignore b/.gitignore index a06c3da7f..f7c6f512e 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,7 @@ data/conf/sogo/cron.creds data/conf/sogo/custom-fulllogo.svg data/conf/sogo/custom-shortlogo.svg data/conf/sogo/custom-fulllogo.png +data/conf/acme/dns-01.conf data/gitea/ data/gogs/ data/hooks/dovecot/* @@ -63,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*/ diff --git a/README.md b/README.md index 40288f10e..63d0b6480 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,15 @@ A big thank you to everyone supporting us on GitHub Sponsors—your contribution + ### 50$/Month Sponsors ## Info, documentation and support diff --git a/_modules/scripts/new_options.sh b/_modules/scripts/new_options.sh index 30c747b70..2c39c8a7c 100644 --- a/_modules/scripts/new_options.sh +++ b/_modules/scripts/new_options.sh @@ -57,11 +57,14 @@ adapt_new_options() { "DISABLE_NETFILTER_ISOLATION_RULE" "HTTP_REDIRECT" "ENABLE_IPV6" + "ACME_DNS_CHALLENGE" + "ACME_DNS_PROVIDER" + "ACME_ACCOUNT_EMAIL" ) sed -i --follow-symlinks '$a\' mailcow.conf for option in ${CONFIG_ARRAY[@]}; do - if grep -q "${option}" mailcow.conf; then + if grep -q "^#\?${option}=" mailcow.conf; then continue fi @@ -292,6 +295,20 @@ adapt_new_options() { echo '# This key is used to encrypt email addresses within SOGo URLs' >> mailcow.conf echo "SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C /dev/null | head -c 16)" >> mailcow.conf ;; + ACME_DNS_CHALLENGE) + echo '# Enable DNS-01 challenge for ACME (acme-mailcow) - y/n' >> mailcow.conf + echo '# This requires you to set ACME_DNS_PROVIDER and ACME_ACCOUNT_EMAIL below' >> mailcow.conf + echo 'ACME_DNS_CHALLENGE=n' >> mailcow.conf + ;; + ACME_DNS_PROVIDER) + echo '# DNS provider for DNS-01 challenge (e.g. dns_cf, dns_azure, dns_gd, etc.)' >> mailcow.conf + echo '# See the dns-01 provider documentation for more information.' >> mailcow.conf + echo 'ACME_DNS_PROVIDER=dns_xxx' >> mailcow.conf + ;; + ACME_ACCOUNT_EMAIL) + echo '# Account email for ACME DNS-01 challenge registration' >> mailcow.conf + echo 'ACME_ACCOUNT_EMAIL=me@example.com' >> mailcow.conf + ;; *) echo "${option}=" >> mailcow.conf ;; diff --git a/data/Dockerfiles/acme/Dockerfile b/data/Dockerfiles/acme/Dockerfile index f6e990e57..193ca7e00 100644 --- a/data/Dockerfiles/acme/Dockerfile +++ b/data/Dockerfiles/acme/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21 +FROM alpine:3.23 LABEL maintainer = "The Infrastructure Company GmbH " @@ -14,11 +14,22 @@ RUN apk upgrade --no-cache \ tini \ tzdata \ python3 \ - acme-tiny + acme-tiny \ + git \ + socat \ + && git clone --depth 1 https://github.com/acmesh-official/acme.sh.git /opt/acme.sh \ + && chmod +x /opt/acme.sh/acme.sh \ + && mkdir -p /var/lib/acme/acme-sh + +ENV ACME_SH_BIN=/opt/acme.sh/acme.sh \ + ACME_SH_HOME=/opt/acme.sh \ + ACME_SH_CONFIG_HOME=/var/lib/acme/acme-sh COPY acme.sh /srv/acme.sh COPY functions.sh /srv/functions.sh COPY obtain-certificate.sh /srv/obtain-certificate.sh +COPY obtain-certificate-dns.sh /srv/obtain-certificate-dns.sh +COPY load-dns-config.sh /srv/load-dns-config.sh COPY reload-configurations.sh /srv/reload-configurations.sh COPY expand6.sh /srv/expand6.sh diff --git a/data/Dockerfiles/acme/acme.sh b/data/Dockerfiles/acme/acme.sh index d1b2997e8..6b7b74300 100755 --- a/data/Dockerfiles/acme/acme.sh +++ b/data/Dockerfiles/acme/acme.sh @@ -14,6 +14,17 @@ until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do sleep 2 done +# Create DNS-01 configuration template if it doesn't exist +if [[ ! -f /etc/acme/dns-01.conf ]]; then + mkdir -p /etc/acme + cat > /etc/acme/dns-01.conf <<'EOF' +# Add here your DNS-01 challenge configuration +# For more information, visit the acme.sh documentation: +# https://github.com/acmesh-official/acme.sh/wiki/dnsapi +EOF + echo "Created DNS-01 configuration template at /etc/acme/dns-01.conf" +fi + source /srv/functions.sh # Thanks to https://github.com/cvmiller -> https://github.com/cvmiller/expand6 source /srv/expand6.sh @@ -42,17 +53,21 @@ if [[ "${ENABLE_SSL_SNI}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then ENABLE_SSL_SNI=y fi +if [[ "${ACME_DNS_CHALLENGE}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + ACME_DNS_CHALLENGE=y +fi + if [[ "${SKIP_LETS_ENCRYPT}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then log_f "SKIP_LETS_ENCRYPT=y, skipping Let's Encrypt..." sleep 365d exec $(readlink -f "$0") fi -log_f "Waiting for Controller .." -until ping controller -c1 > /dev/null; do +log_f "Waiting for Docker API..." +until ping dockerapi -c1 > /dev/null; do sleep 1 done -log_f "Controller OK" +log_f "Docker API OK" log_f "Waiting for Postfix..." until ping postfix -c1 > /dev/null; do @@ -234,14 +249,45 @@ 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 - if [[ "${SUBDOMAIN}.${SQL_DOMAIN}" != "${MAILCOW_HOSTNAME}" ]]; then - if check_domain "${SUBDOMAIN}.${SQL_DOMAIN}"; then - VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${SUBDOMAIN}.${SQL_DOMAIN}") - fi + 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 + fi + # Skip if subdomain is covered by a wildcard in ADDITIONAL_SAN + if is_covered_by_wildcard "${FULL_SUBDOMAIN}"; then + log_f "Subdomain '${FULL_SUBDOMAIN}' is covered by wildcard - skipping explicit subdomain" + continue + fi + # Validate and add subdomain + if check_domain "${FULL_SUBDOMAIN}"; then + VALIDATED_CONFIG_DOMAINS_SUBDOMAINS+=("${FULL_SUBDOMAIN}") fi done VALIDATED_CONFIG_DOMAINS+=("${VALIDATED_CONFIG_DOMAINS_SUBDOMAINS[*]}") @@ -258,7 +304,10 @@ while true; do fi # Only add mta-sts subdomain for alias domains if [[ "mta-sts.${alias_domain}" != "${MAILCOW_HOSTNAME}" ]]; then - if check_domain "mta-sts.${alias_domain}"; then + # Skip if mta-sts subdomain is covered by a wildcard + if is_covered_by_wildcard "mta-sts.${alias_domain}"; then + log_f "Alias domain mta-sts subdomain 'mta-sts.${alias_domain}' is covered by wildcard - skipping" + elif check_domain "mta-sts.${alias_domain}"; then VALIDATED_CONFIG_DOMAINS+=("mta-sts.${alias_domain}") fi fi @@ -293,13 +342,31 @@ while true; do done fi + # Check if MAILCOW_HOSTNAME is covered by a wildcard in ADDITIONAL_SAN + MAILCOW_HOSTNAME_COVERED=0 + if [[ ! -z ${VALIDATED_MAILCOW_HOSTNAME} ]]; then + if is_covered_by_wildcard "${VALIDATED_MAILCOW_HOSTNAME}"; then + MAILCOW_PARENT_DOMAIN=$(echo ${VALIDATED_MAILCOW_HOSTNAME} | cut -d. -f2-) + log_f "MAILCOW_HOSTNAME '${VALIDATED_MAILCOW_HOSTNAME}' is covered by wildcard '*.${MAILCOW_PARENT_DOMAIN}' - skipping explicit hostname" + MAILCOW_HOSTNAME_COVERED=1 + fi + fi + # Unique domains for server certificate if [[ ${ENABLE_SSL_SNI} == "y" ]]; then # create certificate for server name and fqdn SANs only - SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) + if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then + SERVER_SAN_VALIDATED=($(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) + else + SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) + fi else # create certificate for all domains, including all subdomains from other domains [*] - SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) + if [[ ${MAILCOW_HOSTNAME_COVERED} == "1" ]]; then + SERVER_SAN_VALIDATED=($(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) + else + SERVER_SAN_VALIDATED=(${VALIDATED_MAILCOW_HOSTNAME} $(echo ${VALIDATED_CONFIG_DOMAINS[*]} ${ADDITIONAL_VALIDATED_SAN[*]} | xargs -n1 | sort -u | xargs)) + fi fi if [[ ! -z ${SERVER_SAN_VALIDATED[*]} ]]; then CERT_NAME=${SERVER_SAN_VALIDATED[0]} diff --git a/data/Dockerfiles/acme/functions.sh b/data/Dockerfiles/acme/functions.sh index 183be01b0..707f46950 100644 --- a/data/Dockerfiles/acme/functions.sh +++ b/data/Dockerfiles/acme/functions.sh @@ -80,6 +80,11 @@ check_domain(){ return 1 fi fi + + if [[ ${ACME_DNS_CHALLENGE} == "y" ]]; then + log_f "ACME_DNS_CHALLENGE=y - skipping IP and HTTP validation for ${DOMAIN}" + return 0 + fi # Check if CNAME without v6 enabled target if [[ ! -z ${AAAA_DOMAIN} ]] && [[ -z $(echo ${AAAA_DOMAIN} | grep "^\([0-9a-fA-F]\{0,4\}:\)\{1,7\}[0-9a-fA-F]\{0,4\}$") ]]; then AAAA_DOMAIN= @@ -130,3 +135,32 @@ verify_challenge_path(){ return 1 fi } + +# Check if a domain is covered by a wildcard (*.example.com) in ADDITIONAL_SAN +# Usage: is_covered_by_wildcard "subdomain.example.com" +# Returns: 0 if covered, 1 if not covered +# Note: Only returns 0 (covered) when DNS-01 challenge is enabled, +# as wildcards cannot be validated with HTTP-01 challenge +is_covered_by_wildcard() { + local DOMAIN=$1 + + # Only skip if DNS challenge is enabled (wildcards require DNS-01) + if [[ ${ACME_DNS_CHALLENGE} != "y" ]]; then + return 1 + fi + + # Return early if no ADDITIONAL_SAN is set + if [[ -z ${ADDITIONAL_SAN} ]]; then + return 1 + fi + + # Extract parent domain (e.g., mail.example.com -> example.com) + local PARENT_DOMAIN=$(echo ${DOMAIN} | cut -d. -f2-) + + # Check if ADDITIONAL_SAN contains a wildcard for this parent domain + if [[ "${ADDITIONAL_SAN}" == *"*.${PARENT_DOMAIN}"* ]]; then + return 0 # Covered by wildcard + fi + + return 1 # Not covered +} diff --git a/data/Dockerfiles/acme/load-dns-config.sh b/data/Dockerfiles/acme/load-dns-config.sh new file mode 100755 index 000000000..af9caeaf6 --- /dev/null +++ b/data/Dockerfiles/acme/load-dns-config.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +SCRIPT_SOURCE="${BASH_SOURCE[0]:-${0}}" +if [[ "${SCRIPT_SOURCE}" == "${0}" ]]; then + __dns_loader_standalone=1 +else + __dns_loader_standalone=0 +fi + +CONFIG_PATH="${ACME_DNS_CONFIG_FILE:-/etc/acme/dns-01.conf}" + +if [[ ! -f "${CONFIG_PATH}" ]]; then + if [[ $__dns_loader_standalone -eq 1 ]]; then + exit 0 + else + return 0 + fi +fi + +source /srv/functions.sh + +log_f "Loading DNS-01 configuration from ${CONFIG_PATH}" + +LINE_NO=0 +while IFS= read -r line || [[ -n "${line}" ]]; do + LINE_NO=$((LINE_NO+1)) + line="${line%$'\r'}" + line_trimmed="$(printf '%s' "${line}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + [[ -z "${line_trimmed}" ]] && continue + [[ "${line_trimmed:0:1}" == "#" ]] && continue + if [[ "${line_trimmed}" != *=* ]]; then + log_f "Skipping invalid DNS config line ${LINE_NO} (missing key=value)" + continue + fi + KEY="${line_trimmed%%=*}" + VALUE="${line_trimmed#*=}" + KEY="$(printf '%s' "${KEY}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + VALUE="$(printf '%s' "${VALUE}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + if [[ -z "${KEY}" ]]; then + log_f "Skipping invalid DNS config line ${LINE_NO} (empty key)" + continue + fi + if [[ "${VALUE}" =~ ^\".*\"$ ]]; then + VALUE="${VALUE:1:-1}" + elif [[ "${VALUE}" =~ ^\'.*\'$ ]]; then + VALUE="${VALUE:1:-1}" + fi + export "${KEY}"="${VALUE}" + log_f "Exported DNS config key ${KEY}" + +done < "${CONFIG_PATH}" + +if [[ $__dns_loader_standalone -eq 1 ]]; then + exit 0 +else + return 0 +fi diff --git a/data/Dockerfiles/acme/obtain-certificate-dns.sh b/data/Dockerfiles/acme/obtain-certificate-dns.sh new file mode 100644 index 000000000..f88089f01 --- /dev/null +++ b/data/Dockerfiles/acme/obtain-certificate-dns.sh @@ -0,0 +1,177 @@ +#!/bin/bash + +# Return values / exit codes +# 0 = cert created successfully +# 1 = cert renewed successfully +# 2 = cert not due for renewal +# * = errors + +source /srv/functions.sh + +CERT_DOMAINS=(${DOMAINS[@]}) +CERT_DOMAIN=${CERT_DOMAINS[0]} +ACME_BASE=/var/lib/acme + +# Load optional DNS provider secrets from /etc/acme/dns-01.conf +if [[ -f /srv/load-dns-config.sh ]]; then + source /srv/load-dns-config.sh + if declare -F log_f >/dev/null; then + log_f "ACME_DNS_CHALLENGE is enabled, DNS provider secrets loaded" + fi +fi + +TYPE=${1} +PREFIX="" +# only support rsa certificates for now +if [[ "${TYPE}" != "rsa" ]]; then + log_f "Unknown certificate type '${TYPE}' requested" + exit 5 +fi + +if [[ -z "${ACME_DNS_PROVIDER}" ]]; then + log_f "ACME_DNS_PROVIDER is required when ACME_DNS_CHALLENGE is enabled" + exit 6 +fi + +DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains +CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem +SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist +KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem +CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr + +if [[ -z ${CERT_DOMAINS[*]} ]]; then + log_f "Missing CERT_DOMAINS to obtain a certificate" + exit 3 +fi + +if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + if [[ ! -z "${DIRECTORY_URL}" ]]; then + log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL" + fi + log_f "Using Let's Encrypt staging servers" + ACME_SH_SERVER_ARGS=("--staging") +elif [[ ! -z "${DIRECTORY_URL}" ]]; then + log_f "Using custom directory URL ${DIRECTORY_URL}" + ACME_SH_SERVER_ARGS=("--server" "${DIRECTORY_URL}") +else + log_f "Using Let's Encrypt production servers" + ACME_SH_SERVER_ARGS=("--server" "letsencrypt") +fi + +if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then + if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then + log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining" + elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then + log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing" + else + log_f "Certificate ${CERT} validation done, neither changed nor due for renewal." + exit 2 + fi +else + log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining" +fi + +# Make backup +if [[ -f ${CERT} ]]; then + DATE=$(date +%Y-%m-%d_%H_%M_%S) + BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE} + log_f "Creating backups in ${BACKUP_DIR} ..." + mkdir -p ${BACKUP_DIR}/ + [[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/ + [[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/ + [[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/ + [[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/ +fi + +mkdir -p ${ACME_BASE}/${CERT_DOMAIN} +if [[ ! -f ${KEY} ]]; then + log_f "Copying shared private key for this certificate..." + cp ${SHARED_KEY} ${KEY} + chmod 600 ${KEY} +fi + +# Generating CSR to keep layout parity with HTTP challenge flow +printf "[SAN]\nsubjectAltName=" > /tmp/_SAN +printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN +sed -i '$s/,$//' /tmp/_SAN +openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat "$(openssl version -d | sed 's/.*\"\(.*\)\"/\1/g')/openssl.cnf" /tmp/_SAN) > ${CSR} + +log_f "Checking resolver..." +until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do + sleep 2 +done +log_f "Resolver OK" + +ACME_SH_BIN_PATH=${ACME_SH_BIN:-/opt/acme.sh/acme.sh} +ACME_SH_WORK_HOME=${ACME_SH_CONFIG_HOME:-/var/lib/acme/acme-sh} +mkdir -p ${ACME_SH_WORK_HOME} + +if [[ ! -x ${ACME_SH_BIN_PATH} ]]; then + log_f "acme.sh binary not found at ${ACME_SH_BIN_PATH}" + exit 7 +fi + +if [[ ! -f ${ACME_SH_WORK_HOME}/account.conf ]]; then + if [[ -z "${ACME_ACCOUNT_EMAIL}" ]]; then + log_f "ACME_ACCOUNT_EMAIL is required to register a new acme.sh account" + exit 8 + fi + log_f "Registering acme.sh account for ${ACME_ACCOUNT_EMAIL}" + REGISTER_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}" "--register-account" "-m" "${ACME_ACCOUNT_EMAIL}") + REGISTER_CMD+=("${ACME_SH_SERVER_ARGS[@]}") + REGISTER_RESPONSE=$("${REGISTER_CMD[@]}" 2>&1) + if [[ $? -ne 0 ]]; then + log_f "Failed to register acme.sh account: ${REGISTER_RESPONSE}" + exit 9 + fi +fi + +TMP_CERT=$(mktemp /tmp/acme-cert.XXXXXX) +TMP_FULLCHAIN=$(mktemp /tmp/acme-fullchain.XXXXXX) + +ACME_CMD=("${ACME_SH_BIN_PATH}" "--home" "${ACME_SH_WORK_HOME}" "--config-home" "${ACME_SH_WORK_HOME}" "--cert-home" "${ACME_SH_WORK_HOME}") +ACME_CMD+=("${ACME_SH_SERVER_ARGS[@]}") +ACME_CMD+=("--issue" "--dns" "${ACME_DNS_PROVIDER}" "--key-file" "${KEY}" "--cert-file" "${TMP_CERT}" "--fullchain-file" "${TMP_FULLCHAIN}" "--force") +for domain in "${CERT_DOMAINS[@]}"; do + ACME_CMD+=("-d" "${domain}") +done + +log_f "Using command ${ACME_CMD[*]}" +if [[ -n "${ACME_DNS_PROVIDER}" ]]; then + log_f "DNS provider: ${ACME_DNS_PROVIDER}" +fi +if compgen -A variable | grep -Eq "^DNS_|^ACME_"; then + LOG_KEYS=$(env | grep -E "^(DNS_|ACME_)" | cut -d= -f1 | tr '\n' ' ') + log_f "Available DNS/ACME env keys: ${LOG_KEYS}" redis_only +fi +ACME_RESPONSE=$("${ACME_CMD[@]}" 2>&1 | tee /dev/fd/5; exit ${PIPESTATUS[0]}) +SUCCESS="$?" +ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64) +log_f "${ACME_RESPONSE_B64}" redis_only b64 + +case "$SUCCESS" in + 0) + log_f "Deploying certificate ${CERT}..." + if verify_hash_match ${TMP_FULLCHAIN} ${KEY}; then + RETURN=0 + if [[ -f ${CERT} ]]; then + RETURN=1 + fi + mv -f ${TMP_FULLCHAIN} ${CERT} + rm -f ${TMP_CERT} + echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE} + log_f "Certificate successfully obtained via DNS challenge" + exit ${RETURN} + else + log_f "Certificate was requested, but key and certificate hashes do not match" + rm -f ${TMP_CERT} ${TMP_FULLCHAIN} + exit 4 + fi + ;; + *) + log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}' via DNS challenge" + redis-cli -h redis -a ${REDISPASS} --no-auth-warning SET ACME_FAIL_TIME "$(date +%s)" + rm -f ${TMP_CERT} ${TMP_FULLCHAIN} + exit 100${SUCCESS} + ;; +esac diff --git a/data/Dockerfiles/acme/obtain-certificate.sh b/data/Dockerfiles/acme/obtain-certificate.sh index f476bf666..9d727a202 100644 --- a/data/Dockerfiles/acme/obtain-certificate.sh +++ b/data/Dockerfiles/acme/obtain-certificate.sh @@ -20,6 +20,10 @@ if [[ "${TYPE}" != "rsa" ]]; then log_f "Unknown certificate type '${TYPE}' requested" exit 5 fi + +if [[ "${ACME_DNS_CHALLENGE}" == "y" ]]; then + exec /srv/obtain-certificate-dns.sh "$@" +fi DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist diff --git a/data/Dockerfiles/acme/reload-configurations.sh b/data/Dockerfiles/acme/reload-configurations.sh index d8dc1e1bb..8d194b68b 100644 --- a/data/Dockerfiles/acme/reload-configurations.sh +++ b/data/Dockerfiles/acme/reload-configurations.sh @@ -2,32 +2,32 @@ # Reading container IDs # Wrapping as array to ensure trimmed content when calling $NGINX etc. -NGINX=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) -DOVECOT=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) -POSTFIX=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) +NGINX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"nginx-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) +DOVECOT=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"dovecot-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) +POSTFIX=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" | tr "\n" " ")) reload_nginx(){ echo "Reloading Nginx..." - NGINX_RELOAD_RET=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type) + NGINX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${NGINX}/exec -d '{"cmd":"reload", "task":"nginx"}' --silent -H 'Content-type: application/json' | jq -r .type) [[ ${NGINX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Nginx, restarting container..."; restart_container ${NGINX} ; } } reload_dovecot(){ echo "Reloading Dovecot..." - DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type) + DOVECOT_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${DOVECOT}/exec -d '{"cmd":"reload", "task":"dovecot"}' --silent -H 'Content-type: application/json' | jq -r .type) [[ ${DOVECOT_RELOAD_RET} != 'success' ]] && { echo "Could not reload Dovecot, restarting container..."; restart_container ${DOVECOT} ; } } reload_postfix(){ echo "Reloading Postfix..." - POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type) + POSTFIX_RELOAD_RET=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/exec -d '{"cmd":"reload", "task":"postfix"}' --silent -H 'Content-type: application/json' | jq -r .type) [[ ${POSTFIX_RELOAD_RET} != 'success' ]] && { echo "Could not reload Postfix, restarting container..."; restart_container ${POSTFIX} ; } } restart_container(){ for container in $*; do echo "Restarting ${container}..." - C_REST_OUT=$(curl -X POST --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg') + C_REST_OUT=$(curl -X POST --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${container}/restart --silent | jq -r '.msg') echo "${C_REST_OUT}" done } diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index e60e7eef1..5f444a8f4 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -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 " diff --git a/data/Dockerfiles/controller/docker-entrypoint.sh b/data/Dockerfiles/controller/docker-entrypoint.sh deleted file mode 100755 index c0a8193f0..000000000 --- a/data/Dockerfiles/controller/docker-entrypoint.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -`openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ - -keyout /app/controller_key.pem \ - -out /app/controller_cert.pem \ - -subj /CN=controller/O=mailcow \ - -addext subjectAltName=DNS:controller` - -exec "$@" diff --git a/data/Dockerfiles/controller/mailcow-adm/mailcow-adm.py b/data/Dockerfiles/controller/mailcow-adm/mailcow-adm.py deleted file mode 100755 index 18eaaea0d..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/mailcow-adm.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import argparse -import sys - -from models.AliasModel import AliasModel -from models.MailboxModel import MailboxModel -from models.SyncjobModel import SyncjobModel -from models.CalendarModel import CalendarModel -from models.MailerModel import MailerModel -from models.AddressbookModel import AddressbookModel -from models.MaildirModel import MaildirModel -from models.DomainModel import DomainModel -from models.DomainadminModel import DomainadminModel -from models.StatusModel import StatusModel - -from modules.Utils import Utils - - - - -def main(): - utils = Utils() - - model_map = { - MailboxModel.parser_command: MailboxModel, - AliasModel.parser_command: AliasModel, - SyncjobModel.parser_command: SyncjobModel, - CalendarModel.parser_command: CalendarModel, - AddressbookModel.parser_command: AddressbookModel, - MailerModel.parser_command: MailerModel, - MaildirModel.parser_command: MaildirModel, - DomainModel.parser_command: DomainModel, - DomainadminModel.parser_command: DomainadminModel, - StatusModel.parser_command: StatusModel - } - - parser = argparse.ArgumentParser(description="mailcow Admin Tool") - subparsers = parser.add_subparsers(dest="command", required=True) - - for model in model_map.values(): - model.add_parser(subparsers) - - args = parser.parse_args() - - - for cmd, model_cls in model_map.items(): - if args.command == cmd and model_cls.has_required_args(args): - instance = model_cls(**vars(args)) - action = getattr(instance, args.object, None) - if callable(action): - res = action() - utils.pprint(res) - sys.exit(0) - - parser.print_help() - - -if __name__ == "__main__": - main() diff --git a/data/Dockerfiles/controller/mailcow-adm/models/AddressbookModel.py b/data/Dockerfiles/controller/mailcow-adm/models/AddressbookModel.py deleted file mode 100644 index eff99fc46..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/AddressbookModel.py +++ /dev/null @@ -1,140 +0,0 @@ -from modules.Sogo import Sogo -from models.BaseModel import BaseModel - -class AddressbookModel(BaseModel): - parser_command = "addressbook" - required_args = { - "add": [["username", "name"]], - "delete": [["username", "name"]], - "get": [["username", "name"]], - "set_acl": [["username", "name", "sharee_email", "acl"]], - "get_acl": [["username", "name"]], - "delete_acl": [["username", "name", "sharee_email"]], - "add_contact": [["username", "name", "contact_name", "contact_email", "type"]], - "delete_contact": [["username", "name", "contact_name"]], - } - - def __init__( - self, - username=None, - name=None, - sharee_email=None, - acl=None, - subscribe=None, - ics=None, - contact_name=None, - contact_email=None, - type=None, - **kwargs - ): - self.sogo = Sogo(username) - - self.name = name - self.acl = acl - self.sharee_email = sharee_email - self.subscribe = subscribe - self.ics = ics - self.contact_name = contact_name - self.contact_email = contact_email - self.type = type - - def add(self): - """ - Add a new addressbook. - :return: Response from SOGo API. - """ - return self.sogo.addAddressbook(self.name) - - def set_acl(self): - """ - Set ACL for the addressbook. - :return: Response from SOGo API. - """ - addressbook_id = self.sogo.getAddressbookIdByName(self.name) - if not addressbook_id: - print(f"Addressbook '{self.name}' not found for user '{self.username}'.") - return None - return self.sogo.setAddressbookACL(addressbook_id, self.sharee_email, self.acl, self.subscribe) - - def delete_acl(self): - """ - Delete the addressbook ACL. - :return: Response from SOGo API. - """ - addressbook_id = self.sogo.getAddressbookIdByName(self.name) - if not addressbook_id: - print(f"Addressbook '{self.name}' not found for user '{self.username}'.") - return None - return self.sogo.deleteAddressbookACL(addressbook_id, self.sharee_email) - - def get_acl(self): - """ - Get the ACL for the addressbook. - :return: Response from SOGo API. - """ - addressbook_id = self.sogo.getAddressbookIdByName(self.name) - if not addressbook_id: - print(f"Addressbook '{self.name}' not found for user '{self.username}'.") - return None - return self.sogo.getAddressbookACL(addressbook_id) - - def add_contact(self): - """ - Add a new contact to the addressbook. - :return: Response from SOGo API. - """ - addressbook_id = self.sogo.getAddressbookIdByName(self.name) - if not addressbook_id: - print(f"Addressbook '{self.name}' not found for user '{self.username}'.") - return None - if self.type == "card": - return self.sogo.addAddressbookContact(addressbook_id, self.contact_name, self.contact_email) - elif self.type == "list": - return self.sogo.addAddressbookContactList(addressbook_id, self.contact_name, self.contact_email) - - def delete_contact(self): - """ - Delete a contact or contactlist from the addressbook. - :return: Response from SOGo API. - """ - addressbook_id = self.sogo.getAddressbookIdByName(self.name) - if not addressbook_id: - print(f"Addressbook '{self.name}' not found for user '{self.username}'.") - return None - return self.sogo.deleteAddressbookItem(addressbook_id, self.contact_name) - - def get(self): - """ - Retrieve addressbooks list. - :return: Response from SOGo API. - """ - return self.sogo.getAddressbookList() - - def delete(self): - """ - Delete the addressbook. - :return: Response from SOGo API. - """ - - addressbook_id = self.sogo.getAddressbookIdByName(self.name) - if not addressbook_id: - print(f"Addressbook '{self.name}' not found for user '{self.username}'.") - return None - return self.sogo.deleteAddressbook(addressbook_id) - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Manage addressbooks (add, delete, get, set_acl, get_acl, delete_acl, add_contact, delete_contact)" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, set_acl, get_acl, delete_acl, add_contact, delete_contact") - parser.add_argument("--username", required=True, help="Username of the addressbook owner (e.g. user@example.com)") - parser.add_argument("--name", help="Addressbook name") - parser.add_argument("--sharee-email", help="Email address to share the addressbook with") - parser.add_argument("--acl", help="ACL rights for the sharee (e.g. r, w, rw)") - parser.add_argument("--subscribe", action='store_true', help="Subscribe the sharee to the addressbook") - parser.add_argument("--contact-name", help="Name of the contact or contactlist to add or delete") - parser.add_argument("--contact-email", help="Email address of the contact to add") - parser.add_argument("--type", choices=["card", "list"], help="Type of contact to add: card (single contact) or list (distribution list)") - diff --git a/data/Dockerfiles/controller/mailcow-adm/models/AliasModel.py b/data/Dockerfiles/controller/mailcow-adm/models/AliasModel.py deleted file mode 100644 index 384b7bd58..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/AliasModel.py +++ /dev/null @@ -1,107 +0,0 @@ -from modules.Mailcow import Mailcow -from models.BaseModel import BaseModel - -class AliasModel(BaseModel): - parser_command = "alias" - required_args = { - "add": [["address", "goto"]], - "delete": [["id"]], - "get": [["id"]], - "edit": [["id"]] - } - - def __init__( - self, - id=None, - address=None, - goto=None, - active=None, - sogo_visible=None, - **kwargs - ): - self.mailcow = Mailcow() - - self.id = id - self.address = address - self.goto = goto - self.active = active - self.sogo_visible = sogo_visible - - @classmethod - def from_dict(cls, data): - return cls( - address=data.get("address"), - goto=data.get("goto"), - active=data.get("active", None), - sogo_visible=data.get("sogo_visible", None) - ) - - def getAdd(self): - """ - Get the alias details as a dictionary for adding, sets default values. - :return: Dictionary containing alias details. - """ - - alias = { - "address": self.address, - "goto": self.goto, - "active": self.active if self.active is not None else 1, - "sogo_visible": self.sogo_visible if self.sogo_visible is not None else 0 - } - return {key: value for key, value in alias.items() if value is not None} - - def getEdit(self): - """ - Get the alias details as a dictionary for editing, sets no default values. - :return: Dictionary containing mailbox details. - """ - - alias = { - "address": self.address, - "goto": self.goto, - "active": self.active, - "sogo_visible": self.sogo_visible - } - return {key: value for key, value in alias.items() if value is not None} - - def get(self): - """ - Get the mailbox details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.getAlias(self.id) - - def delete(self): - """ - Get the mailbox details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.deleteAlias(self.id) - - def add(self): - """ - Get the mailbox details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.addAlias(self.getAdd()) - - def edit(self): - """ - Get the mailbox details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.editAlias(self.id, self.getEdit()) - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Manage aliases (add, delete, get, edit)" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit") - parser.add_argument("--id", help="Alias object ID (required for get, edit, delete)") - parser.add_argument("--address", help="Alias email address (e.g. alias@example.com)") - parser.add_argument("--goto", help="Destination address(es), comma-separated (e.g. user1@example.com,user2@example.com)") - parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the alias") - parser.add_argument("--sogo-visible", choices=["1", "0"], help="Show alias in SOGo addressbook (1 = yes, 0 = no)") - diff --git a/data/Dockerfiles/controller/mailcow-adm/models/BaseModel.py b/data/Dockerfiles/controller/mailcow-adm/models/BaseModel.py deleted file mode 100644 index 593724d31..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/BaseModel.py +++ /dev/null @@ -1,35 +0,0 @@ -class BaseModel: - parser_command = "" - required_args = {} - - @classmethod - def has_required_args(cls, args): - """ - Validate that all required arguments are present. - """ - object_name = args.object if hasattr(args, "object") else args.get("object") - required_lists = cls.required_args.get(object_name, False) - - if not required_lists: - return False - - for required_set in required_lists: - result = True - for required_args in required_set: - if isinstance(args, dict): - if not args.get(required_args): - result = False - break - elif not hasattr(args, required_args): - result = False - break - if result: - break - - if not result: - print(f"Required arguments for '{object_name}': {required_lists}") - return result - - @classmethod - def add_parser(cls, subparsers): - pass diff --git a/data/Dockerfiles/controller/mailcow-adm/models/CalendarModel.py b/data/Dockerfiles/controller/mailcow-adm/models/CalendarModel.py deleted file mode 100644 index 1390d4ffa..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/CalendarModel.py +++ /dev/null @@ -1,111 +0,0 @@ -from modules.Sogo import Sogo -from models.BaseModel import BaseModel - -class CalendarModel(BaseModel): - parser_command = "calendar" - required_args = { - "add": [["username", "name"]], - "delete": [["username", "name"]], - "get": [["username"]], - "import_ics": [["username", "name", "ics"]], - "set_acl": [["username", "name", "sharee_email", "acl"]], - "get_acl": [["username", "name"]], - "delete_acl": [["username", "name", "sharee_email"]], - } - - def __init__( - self, - username=None, - name=None, - sharee_email=None, - acl=None, - subscribe=None, - ics=None, - **kwargs - ): - self.sogo = Sogo(username) - - self.name = name - self.acl = acl - self.sharee_email = sharee_email - self.subscribe = subscribe - self.ics = ics - - def add(self): - """ - Add a new calendar. - :return: Response from SOGo API. - """ - return self.sogo.addCalendar(self.name) - - def delete(self): - """ - Delete a calendar. - :return: Response from SOGo API. - """ - calendar_id = self.sogo.getCalendarIdByName(self.name) - if not calendar_id: - print(f"Calendar '{self.name}' not found for user '{self.username}'.") - return None - return self.sogo.deleteCalendar(calendar_id) - - def get(self): - """ - Get the calendar details. - :return: Response from SOGo API. - """ - return self.sogo.getCalendar() - - def set_acl(self): - """ - Set ACL for the calendar. - :return: Response from SOGo API. - """ - calendar_id = self.sogo.getCalendarIdByName(self.name) - if not calendar_id: - print(f"Calendar '{self.name}' not found for user '{self.username}'.") - return None - return self.sogo.setCalendarACL(calendar_id, self.sharee_email, self.acl, self.subscribe) - - def delete_acl(self): - """ - Delete the calendar ACL. - :return: Response from SOGo API. - """ - calendar_id = self.sogo.getCalendarIdByName(self.name) - if not calendar_id: - print(f"Calendar '{self.name}' not found for user '{self.username}'.") - return None - return self.sogo.deleteCalendarACL(calendar_id, self.sharee_email) - - def get_acl(self): - """ - Get the ACL for the calendar. - :return: Response from SOGo API. - """ - calendar_id = self.sogo.getCalendarIdByName(self.name) - if not calendar_id: - print(f"Calendar '{self.name}' not found for user '{self.username}'.") - return None - return self.sogo.getCalendarACL(calendar_id) - - def import_ics(self): - """ - Import a calendar from an ICS file. - :return: Response from SOGo API. - """ - return self.sogo.importCalendar(self.name, self.ics) - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Manage calendars (add, delete, get, import_ics, set_acl, get_acl, delete_acl)" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, import_ics, set_acl, get_acl, delete_acl") - parser.add_argument("--username", required=True, help="Username of the calendar owner (e.g. user@example.com)") - parser.add_argument("--name", help="Calendar name") - parser.add_argument("--ics", help="Path to ICS file for import") - parser.add_argument("--sharee-email", help="Email address to share the calendar with") - parser.add_argument("--acl", help="ACL rights for the sharee (e.g. r, w, rw)") - parser.add_argument("--subscribe", action='store_true', help="Subscribe the sharee to the calendar") diff --git a/data/Dockerfiles/controller/mailcow-adm/models/DomainModel.py b/data/Dockerfiles/controller/mailcow-adm/models/DomainModel.py deleted file mode 100644 index 6157300f1..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/DomainModel.py +++ /dev/null @@ -1,162 +0,0 @@ -from modules.Mailcow import Mailcow -from models.BaseModel import BaseModel - -class DomainModel(BaseModel): - parser_command = "domain" - required_args = { - "add": [["domain"]], - "delete": [["domain"]], - "get": [["domain"]], - "edit": [["domain"]] - } - - def __init__( - self, - domain=None, - active=None, - aliases=None, - backupmx=None, - defquota=None, - description=None, - mailboxes=None, - maxquota=None, - quota=None, - relay_all_recipients=None, - rl_frame=None, - rl_value=None, - restart_sogo=None, - tags=None, - **kwargs - ): - self.mailcow = Mailcow() - - self.domain = domain - self.active = active - self.aliases = aliases - self.backupmx = backupmx - self.defquota = defquota - self.description = description - self.mailboxes = mailboxes - self.maxquota = maxquota - self.quota = quota - self.relay_all_recipients = relay_all_recipients - self.rl_frame = rl_frame - self.rl_value = rl_value - self.restart_sogo = restart_sogo - self.tags = tags - - @classmethod - def from_dict(cls, data): - return cls( - domain=data.get("domain"), - active=data.get("active", None), - aliases=data.get("aliases", None), - backupmx=data.get("backupmx", None), - defquota=data.get("defquota", None), - description=data.get("description", None), - mailboxes=data.get("mailboxes", None), - maxquota=data.get("maxquota", None), - quota=data.get("quota", None), - relay_all_recipients=data.get("relay_all_recipients", None), - rl_frame=data.get("rl_frame", None), - rl_value=data.get("rl_value", None), - restart_sogo=data.get("restart_sogo", None), - tags=data.get("tags", None) - ) - - def getAdd(self): - """ - Get the domain details as a dictionary for adding, sets default values. - :return: Dictionary containing domain details. - """ - domain = { - "domain": self.domain, - "active": self.active if self.active is not None else 1, - "aliases": self.aliases if self.aliases is not None else 400, - "backupmx": self.backupmx if self.backupmx is not None else 0, - "defquota": self.defquota if self.defquota is not None else 3072, - "description": self.description if self.description is not None else "", - "mailboxes": self.mailboxes if self.mailboxes is not None else 10, - "maxquota": self.maxquota if self.maxquota is not None else 10240, - "quota": self.quota if self.quota is not None else 10240, - "relay_all_recipients": self.relay_all_recipients if self.relay_all_recipients is not None else 0, - "rl_frame": self.rl_frame, - "rl_value": self.rl_value, - "restart_sogo": self.restart_sogo if self.restart_sogo is not None else 0, - "tags": self.tags if self.tags is not None else [] - } - return {key: value for key, value in domain.items() if value is not None} - - def getEdit(self): - """ - Get the domain details as a dictionary for editing, sets no default values. - :return: Dictionary containing domain details. - """ - domain = { - "domain": self.domain, - "active": self.active, - "aliases": self.aliases, - "backupmx": self.backupmx, - "defquota": self.defquota, - "description": self.description, - "mailboxes": self.mailboxes, - "maxquota": self.maxquota, - "quota": self.quota, - "relay_all_recipients": self.relay_all_recipients, - "rl_frame": self.rl_frame, - "rl_value": self.rl_value, - "restart_sogo": self.restart_sogo, - "tags": self.tags - } - return {key: value for key, value in domain.items() if value is not None} - - def get(self): - """ - Get the domain details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.getDomain(self.domain) - - def delete(self): - """ - Delete the domain from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.deleteDomain(self.domain) - - def add(self): - """ - Add the domain to the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.addDomain(self.getAdd()) - - def edit(self): - """ - Edit the domain in the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.editDomain(self.domain, self.getEdit()) - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Manage domains (add, delete, get, edit)" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit") - parser.add_argument("--domain", required=True, help="Domain name (e.g. domain.tld)") - parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the domain") - parser.add_argument("--aliases", help="Number of aliases allowed for the domain") - parser.add_argument("--backupmx", choices=["1", "0"], help="Enable (1) or disable (0) backup MX") - parser.add_argument("--defquota", help="Default quota for mailboxes in MB") - parser.add_argument("--description", help="Description of the domain") - parser.add_argument("--mailboxes", help="Number of mailboxes allowed for the domain") - parser.add_argument("--maxquota", help="Maximum quota for the domain in MB") - parser.add_argument("--quota", help="Quota used by the domain in MB") - parser.add_argument("--relay-all-recipients", choices=["1", "0"], help="Relay all recipients (1 = yes, 0 = no)") - parser.add_argument("--rl-frame", help="Rate limit frame (e.g., s, m, h)") - parser.add_argument("--rl-value", help="Rate limit value") - parser.add_argument("--restart-sogo", help="Restart SOGo after changes (1 = yes, 0 = no)") - parser.add_argument("--tags", nargs="*", help="Tags for the domain") - diff --git a/data/Dockerfiles/controller/mailcow-adm/models/DomainadminModel.py b/data/Dockerfiles/controller/mailcow-adm/models/DomainadminModel.py deleted file mode 100644 index 0f20b4dff..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/DomainadminModel.py +++ /dev/null @@ -1,106 +0,0 @@ -from modules.Mailcow import Mailcow -from models.BaseModel import BaseModel - -class DomainadminModel(BaseModel): - parser_command = "domainadmin" - required_args = { - "add": [["username", "domains", "password"]], - "delete": [["username"]], - "get": [["username"]], - "edit": [["username"]] - } - - def __init__( - self, - username=None, - domains=None, - password=None, - active=None, - **kwargs - ): - self.mailcow = Mailcow() - - self.username = username - self.domains = domains - self.password = password - self.password2 = password - self.active = active - - @classmethod - def from_dict(cls, data): - return cls( - username=data.get("username"), - domains=data.get("domains"), - password=data.get("password"), - password2=data.get("password"), - active=data.get("active", None), - ) - - def getAdd(self): - """ - Get the domain admin details as a dictionary for adding, sets default values. - :return: Dictionary containing domain admin details. - """ - domainadmin = { - "username": self.username, - "domains": self.domains, - "password": self.password, - "password2": self.password2, - "active": self.active if self.active is not None else "1" - } - return {key: value for key, value in domainadmin.items() if value is not None} - - def getEdit(self): - """ - Get the domain admin details as a dictionary for editing, sets no default values. - :return: Dictionary containing domain admin details. - """ - domainadmin = { - "username": self.username, - "domains": self.domains, - "password": self.password, - "password2": self.password2, - "active": self.active - } - return {key: value for key, value in domainadmin.items() if value is not None} - - def get(self): - """ - Get the domain admin details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.getDomainadmin(self.username) - - def delete(self): - """ - Delete the domain admin from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.deleteDomainadmin(self.username) - - def add(self): - """ - Add the domain admin to the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.addDomainadmin(self.getAdd()) - - def edit(self): - """ - Edit the domain admin in the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.editDomainadmin(self.username, self.getEdit()) - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Manage domain admins (add, delete, get, edit)" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit") - parser.add_argument("--username", help="Username for the domain admin") - parser.add_argument("--domains", help="Comma-separated list of domains") - parser.add_argument("--password", help="Password for the domain admin") - parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the domain admin") - diff --git a/data/Dockerfiles/controller/mailcow-adm/models/MailboxModel.py b/data/Dockerfiles/controller/mailcow-adm/models/MailboxModel.py deleted file mode 100644 index 1608b7334..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/MailboxModel.py +++ /dev/null @@ -1,164 +0,0 @@ -from modules.Mailcow import Mailcow -from models.BaseModel import BaseModel - -class MailboxModel(BaseModel): - parser_command = "mailbox" - required_args = { - "add": [["username", "password"]], - "delete": [["username"]], - "get": [["username"]], - "edit": [["username"]] - } - - def __init__( - self, - password=None, - username=None, - domain=None, - local_part=None, - active=None, - sogo_access=None, - name=None, - authsource=None, - quota=None, - force_pw_update=None, - tls_enforce_in=None, - tls_enforce_out=None, - tags=None, - sender_acl=None, - **kwargs - ): - self.mailcow = Mailcow() - - if username is not None and "@" in username: - self.username = username - self.local_part, self.domain = username.split("@") - else: - self.username = f"{local_part}@{domain}" - self.local_part = local_part - self.domain = domain - - self.password = password - self.password2 = password - self.active = active - self.sogo_access = sogo_access - self.name = name - self.authsource = authsource - self.quota = quota - self.force_pw_update = force_pw_update - self.tls_enforce_in = tls_enforce_in - self.tls_enforce_out = tls_enforce_out - self.tags = tags - self.sender_acl = sender_acl - - @classmethod - def from_dict(cls, data): - return cls( - domain=data.get("domain"), - local_part=data.get("local_part"), - password=data.get("password"), - password2=data.get("password"), - active=data.get("active", None), - sogo_access=data.get("sogo_access", None), - name=data.get("name", None), - authsource=data.get("authsource", None), - quota=data.get("quota", None), - force_pw_update=data.get("force_pw_update", None), - tls_enforce_in=data.get("tls_enforce_in", None), - tls_enforce_out=data.get("tls_enforce_out", None), - tags=data.get("tags", None), - sender_acl=data.get("sender_acl", None) - ) - - def getAdd(self): - """ - Get the mailbox details as a dictionary for adding, sets default values. - :return: Dictionary containing mailbox details. - """ - - mailbox = { - "domain": self.domain, - "local_part": self.local_part, - "password": self.password, - "password2": self.password2, - "active": self.active if self.active is not None else 1, - "name": self.name if self.name is not None else "", - "authsource": self.authsource if self.authsource is not None else "mailcow", - "quota": self.quota if self.quota is not None else 0, - "force_pw_update": self.force_pw_update if self.force_pw_update is not None else 0, - "tls_enforce_in": self.tls_enforce_in if self.tls_enforce_in is not None else 0, - "tls_enforce_out": self.tls_enforce_out if self.tls_enforce_out is not None else 0, - "tags": self.tags if self.tags is not None else [] - } - return {key: value for key, value in mailbox.items() if value is not None} - - def getEdit(self): - """ - Get the mailbox details as a dictionary for editing, sets no default values. - :return: Dictionary containing mailbox details. - """ - - mailbox = { - "domain": self.domain, - "local_part": self.local_part, - "password": self.password, - "password2": self.password2, - "active": self.active, - "name": self.name, - "authsource": self.authsource, - "quota": self.quota, - "force_pw_update": self.force_pw_update, - "tls_enforce_in": self.tls_enforce_in, - "tls_enforce_out": self.tls_enforce_out, - "tags": self.tags - } - return {key: value for key, value in mailbox.items() if value is not None} - - def get(self): - """ - Get the mailbox details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.getMailbox(self.username) - - def delete(self): - """ - Get the mailbox details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.deleteMailbox(self.username) - - def add(self): - """ - Get the mailbox details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.addMailbox(self.getAdd()) - - def edit(self): - """ - Get the mailbox details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.editMailbox(self.username, self.getEdit()) - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Manage mailboxes (add, delete, get, edit)" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit") - parser.add_argument("--username", help="Full email address of the mailbox (e.g. user@example.com)") - parser.add_argument("--password", help="Password for the mailbox (required for add)") - parser.add_argument("--active", choices=["1", "0"], help="Activate (1) or deactivate (0) the mailbox") - parser.add_argument("--sogo-access", choices=["1", "0"], help="Redirect mailbox to SOGo after web login (1 = yes, 0 = no)") - parser.add_argument("--name", help="Display name of the mailbox owner") - parser.add_argument("--authsource", help="Authentication source (default: mailcow)") - parser.add_argument("--quota", help="Mailbox quota in bytes (0 = unlimited)") - parser.add_argument("--force-pw-update", choices=["1", "0"], help="Force password update on next login (1 = yes, 0 = no)") - parser.add_argument("--tls-enforce-in", choices=["1", "0"], help="Enforce TLS for incoming emails (1 = yes, 0 = no)") - parser.add_argument("--tls-enforce-out", choices=["1", "0"], help="Enforce TLS for outgoing emails (1 = yes, 0 = no)") - parser.add_argument("--tags", help="Comma-separated list of tags for the mailbox") - parser.add_argument("--sender-acl", help="Comma-separated list of allowed sender addresses for this mailbox") - diff --git a/data/Dockerfiles/controller/mailcow-adm/models/MaildirModel.py b/data/Dockerfiles/controller/mailcow-adm/models/MaildirModel.py deleted file mode 100644 index b35efb7c1..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/MaildirModel.py +++ /dev/null @@ -1,67 +0,0 @@ -from modules.Dovecot import Dovecot -from models.BaseModel import BaseModel - -class MaildirModel(BaseModel): - parser_command = "maildir" - required_args = { - "encrypt": [], - "decrypt": [], - "restore": [["username", "item"], ["list"]] - } - - def __init__( - self, - username=None, - source=None, - item=None, - overwrite=None, - list=None, - **kwargs - ): - self.dovecot = Dovecot() - - for key, value in kwargs.items(): - setattr(self, key, value) - - self.username = username - self.source = source - self.item = item - self.overwrite = overwrite - self.list = list - - def encrypt(self): - """ - Encrypt the maildir for the specified user or all. - :return: Response from Dovecot. - """ - return self.dovecot.encryptMaildir(self.source_dir, self.output_dir) - - def decrypt(self): - """ - Decrypt the maildir for the specified user or all. - :return: Response from Dovecot. - """ - return self.dovecot.decryptMaildir(self.source_dir, self.output_dir) - - def restore(self): - """ - Restore or List maildir data for the specified user. - :return: Response from Dovecot. - """ - if self.list: - return self.dovecot.listDeletedMaildirs() - return self.dovecot.restoreMaildir(self.username, self.item) - - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Manage maildir (encrypt, decrypt, restore)" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: encrypt, decrypt, restore") - parser.add_argument("--item", help="Item to restore") - parser.add_argument("--username", help="Username to restore the item to") - parser.add_argument("--list", action="store_true", help="List items to restore") - parser.add_argument("--source-dir", help="Path to the source maildir to import/encrypt/decrypt") - parser.add_argument("--output-dir", help="Directory to store encrypted/decrypted files inside the Dovecot container") diff --git a/data/Dockerfiles/controller/mailcow-adm/models/MailerModel.py b/data/Dockerfiles/controller/mailcow-adm/models/MailerModel.py deleted file mode 100644 index 48e4befd1..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/MailerModel.py +++ /dev/null @@ -1,62 +0,0 @@ -import json -from models.BaseModel import BaseModel -from modules.Mailer import Mailer - -class MailerModel(BaseModel): - parser_command = "mail" - required_args = { - "send": [["sender", "recipient", "subject", "body"]] - } - - def __init__( - self, - sender=None, - recipient=None, - subject=None, - body=None, - context=None, - **kwargs - ): - self.sender = sender - self.recipient = recipient - self.subject = subject - self.body = body - self.context = context - - def send(self): - if self.context is not None: - try: - self.context = json.loads(self.context) - except json.JSONDecodeError as e: - return f"Invalid context JSON: {e}" - else: - self.context = {} - - mailer = Mailer( - smtp_host="postfix-mailcow", - smtp_port=25, - username=self.sender, - password="", - use_tls=True - ) - res = mailer.send_mail( - subject=self.subject, - from_addr=self.sender, - to_addrs=self.recipient.split(","), - template=self.body, - context=self.context - ) - return res - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Send emails via SMTP" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: send") - parser.add_argument("--sender", required=True, help="Email sender address") - parser.add_argument("--recipient", required=True, help="Email recipient address (comma-separated for multiple)") - parser.add_argument("--subject", required=True, help="Email subject") - parser.add_argument("--body", required=True, help="Email body (Jinja2 template supported)") - parser.add_argument("--context", help="Context for Jinja2 template rendering (JSON format)") \ No newline at end of file diff --git a/data/Dockerfiles/controller/mailcow-adm/models/StatusModel.py b/data/Dockerfiles/controller/mailcow-adm/models/StatusModel.py deleted file mode 100644 index 8c01d91cb..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/StatusModel.py +++ /dev/null @@ -1,45 +0,0 @@ -from modules.Mailcow import Mailcow -from models.BaseModel import BaseModel - -class StatusModel(BaseModel): - parser_command = "status" - required_args = { - "version": [[]], - "vmail": [[]], - "containers": [[]] - } - - def __init__( - self, - **kwargs - ): - self.mailcow = Mailcow() - - def version(self): - """ - Get the version of the mailcow instance. - :return: Response from the mailcow API. - """ - return self.mailcow.getStatusVersion() - - def vmail(self): - """ - Get the vmail details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.getStatusVmail() - - def containers(self): - """ - Get the status of containers in the mailcow instance. - :return: Response from the mailcow API. - """ - return self.mailcow.getStatusContainers() - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Get information about mailcow (version, vmail, containers)" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: version, vmail, containers") diff --git a/data/Dockerfiles/controller/mailcow-adm/models/SyncjobModel.py b/data/Dockerfiles/controller/mailcow-adm/models/SyncjobModel.py deleted file mode 100644 index 0c100f1c1..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/models/SyncjobModel.py +++ /dev/null @@ -1,221 +0,0 @@ -from modules.Mailcow import Mailcow -from models.BaseModel import BaseModel - -class SyncjobModel(BaseModel): - parser_command = "syncjob" - required_args = { - "add": [["username", "host1", "port1", "user1", "password1", "enc1"]], - "delete": [["id"]], - "get": [["username"]], - "edit": [["id"]], - "run": [["id"]] - } - - def __init__( - self, - id=None, - username=None, - host1=None, - port1=None, - user1=None, - password1=None, - enc1=None, - mins_interval=None, - subfolder2=None, - maxage=None, - maxbytespersecond=None, - timeout1=None, - timeout2=None, - exclude=None, - custom_parameters=None, - delete2duplicates=None, - delete1=None, - delete2=None, - automap=None, - skipcrossduplicates=None, - subscribeall=None, - active=None, - force=None, - **kwargs - ): - self.mailcow = Mailcow() - - for key, value in kwargs.items(): - setattr(self, key, value) - - self.id = id - self.username = username - self.host1 = host1 - self.port1 = port1 - self.user1 = user1 - self.password1 = password1 - self.enc1 = enc1 - self.mins_interval = mins_interval - self.subfolder2 = subfolder2 - self.maxage = maxage - self.maxbytespersecond = maxbytespersecond - self.timeout1 = timeout1 - self.timeout2 = timeout2 - self.exclude = exclude - self.custom_parameters = custom_parameters - self.delete2duplicates = delete2duplicates - self.delete1 = delete1 - self.delete2 = delete2 - self.automap = automap - self.skipcrossduplicates = skipcrossduplicates - self.subscribeall = subscribeall - self.active = active - self.force = force - - @classmethod - def from_dict(cls, data): - return cls( - username=data.get("username"), - host1=data.get("host1"), - port1=data.get("port1"), - user1=data.get("user1"), - password1=data.get("password1"), - enc1=data.get("enc1"), - mins_interval=data.get("mins_interval", None), - subfolder2=data.get("subfolder2", None), - maxage=data.get("maxage", None), - maxbytespersecond=data.get("maxbytespersecond", None), - timeout1=data.get("timeout1", None), - timeout2=data.get("timeout2", None), - exclude=data.get("exclude", None), - custom_parameters=data.get("custom_parameters", None), - delete2duplicates=data.get("delete2duplicates", None), - delete1=data.get("delete1", None), - delete2=data.get("delete2", None), - automap=data.get("automap", None), - skipcrossduplicates=data.get("skipcrossduplicates", None), - subscribeall=data.get("subscribeall", None), - active=data.get("active", None), - ) - - def getAdd(self): - """ - Get the sync job details as a dictionary for adding, sets default values. - :return: Dictionary containing sync job details. - """ - syncjob = { - "username": self.username, - "host1": self.host1, - "port1": self.port1, - "user1": self.user1, - "password1": self.password1, - "enc1": self.enc1, - "mins_interval": self.mins_interval if self.mins_interval is not None else 20, - "subfolder2": self.subfolder2 if self.subfolder2 is not None else "", - "maxage": self.maxage if self.maxage is not None else 0, - "maxbytespersecond": self.maxbytespersecond if self.maxbytespersecond is not None else 0, - "timeout1": self.timeout1 if self.timeout1 is not None else 600, - "timeout2": self.timeout2 if self.timeout2 is not None else 600, - "exclude": self.exclude if self.exclude is not None else "(?i)spam|(?i)junk", - "custom_parameters": self.custom_parameters if self.custom_parameters is not None else "", - "delete2duplicates": 1 if self.delete2duplicates else 0, - "delete1": 1 if self.delete1 else 0, - "delete2": 1 if self.delete2 else 0, - "automap": 1 if self.automap else 0, - "skipcrossduplicates": 1 if self.skipcrossduplicates else 0, - "subscribeall": 1 if self.subscribeall else 0, - "active": 1 if self.active else 0 - } - return {key: value for key, value in syncjob.items() if value is not None} - - def getEdit(self): - """ - Get the sync job details as a dictionary for editing, sets no default values. - :return: Dictionary containing sync job details. - """ - syncjob = { - "username": self.username, - "host1": self.host1, - "port1": self.port1, - "user1": self.user1, - "password1": self.password1, - "enc1": self.enc1, - "mins_interval": self.mins_interval, - "subfolder2": self.subfolder2, - "maxage": self.maxage, - "maxbytespersecond": self.maxbytespersecond, - "timeout1": self.timeout1, - "timeout2": self.timeout2, - "exclude": self.exclude, - "custom_parameters": self.custom_parameters, - "delete2duplicates": self.delete2duplicates, - "delete1": self.delete1, - "delete2": self.delete2, - "automap": self.automap, - "skipcrossduplicates": self.skipcrossduplicates, - "subscribeall": self.subscribeall, - "active": self.active - } - return {key: value for key, value in syncjob.items() if value is not None} - - def get(self): - """ - Get the sync job details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.getSyncjob(self.username) - - def delete(self): - """ - Get the sync job details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.deleteSyncjob(self.id) - - def add(self): - """ - Get the sync job details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.addSyncjob(self.getAdd()) - - def edit(self): - """ - Get the sync job details from the mailcow API. - :return: Response from the mailcow API. - """ - return self.mailcow.editSyncjob(self.id, self.getEdit()) - - def run(self): - """ - Run the sync job. - :return: Response from the mailcow API. - """ - return self.mailcow.runSyncjob(self.id, force=self.force) - - @classmethod - def add_parser(cls, subparsers): - parser = subparsers.add_parser( - cls.parser_command, - help="Manage sync jobs (add, delete, get, edit)" - ) - parser.add_argument("object", choices=list(cls.required_args.keys()), help="Action to perform: add, delete, get, edit") - parser.add_argument("--id", help="Syncjob object ID (required for edit, delete, run)") - parser.add_argument("--username", help="Target mailbox username (e.g. user@example.com)") - parser.add_argument("--host1", help="Source IMAP server hostname") - parser.add_argument("--port1", help="Source IMAP server port") - parser.add_argument("--user1", help="Source IMAP account username") - parser.add_argument("--password1", help="Source IMAP account password") - parser.add_argument("--enc1", choices=["PLAIN", "SSL", "TLS"], help="Encryption for source server connection") - parser.add_argument("--mins-interval", help="Sync interval in minutes (default: 20)") - parser.add_argument("--subfolder2", help="Destination subfolder (default: empty)") - parser.add_argument("--maxage", help="Maximum mail age in days (default: 0 = unlimited)") - parser.add_argument("--maxbytespersecond", help="Maximum bandwidth in bytes/sec (default: 0 = unlimited)") - parser.add_argument("--timeout1", help="Timeout for source server in seconds (default: 600)") - parser.add_argument("--timeout2", help="Timeout for destination server in seconds (default: 600)") - parser.add_argument("--exclude", help="Regex pattern to exclude folders (default: (?i)spam|(?i)junk)") - parser.add_argument("--custom-parameters", help="Additional imapsync parameters") - parser.add_argument("--delete2duplicates", choices=["1", "0"], help="Delete duplicates on destination (1 = yes, 0 = no)") - parser.add_argument("--del1", choices=["1", "0"], help="Delete mails on source after sync (1 = yes, 0 = no)") - parser.add_argument("--del2", choices=["1", "0"], help="Delete mails on destination after sync (1 = yes, 0 = no)") - parser.add_argument("--automap", choices=["1", "0"], help="Enable folder automapping (1 = yes, 0 = no)") - parser.add_argument("--skipcrossduplicates", choices=["1", "0"], help="Skip cross-account duplicates (1 = yes, 0 = no)") - parser.add_argument("--subscribeall", choices=["1", "0"], help="Subscribe to all folders (1 = yes, 0 = no)") - parser.add_argument("--active", choices=["1", "0"], help="Activate syncjob (1 = yes, 0 = no)") - parser.add_argument("--force", action="store_true", help="Force the syncjob to run even if it is not active") - diff --git a/data/Dockerfiles/controller/mailcow-adm/modules/Docker.py b/data/Dockerfiles/controller/mailcow-adm/modules/Docker.py deleted file mode 100644 index d35b3b5d1..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/modules/Docker.py +++ /dev/null @@ -1,128 +0,0 @@ -import docker -from docker.errors import APIError - -class Docker: - def __init__(self): - self.client = docker.from_env() - - def exec_command(self, container_name, cmd, user=None): - """ - Execute a command in a container by its container name. - :param container_name: The name of the container. - :param cmd: The command to execute as a list (e.g., ["ls", "-la"]). - :param user: The user to execute the command as (optional). - :return: A standardized response with status, output, and exit_code. - """ - - filters = {"name": container_name} - - try: - for container in self.client.containers.list(filters=filters): - exec_result = container.exec_run(cmd, user=user) - return { - "status": "success", - "exit_code": exec_result.exit_code, - "output": exec_result.output.decode("utf-8") - } - except APIError as e: - return { - "status": "error", - "exit_code": "APIError", - "output": str(e) - } - except Exception as e: - return { - "status": "error", - "exit_code": "Exception", - "output": str(e) - } - - def start_container(self, container_name): - """ - Start a container by its container name. - :param container_name: The name of the container. - :return: A standardized response with status, output, and exit_code. - """ - - filters = {"name": container_name} - - try: - for container in self.client.containers.list(filters=filters): - container.start() - return { - "status": "success", - "exit_code": "0", - "output": f"Container '{container_name}' started successfully." - } - except APIError as e: - return { - "status": "error", - "exit_code": "APIError", - "output": str(e) - } - except Exception as e: - return { - "status": "error", - "error_type": "Exception", - "output": str(e) - } - - def stop_container(self, container_name): - """ - Stop a container by its container name. - :param container_name: The name of the container. - :return: A standardized response with status, output, and exit_code. - """ - - filters = {"name": container_name} - - try: - for container in self.client.containers.list(filters=filters): - container.stop() - return { - "status": "success", - "exit_code": "0", - "output": f"Container '{container_name}' stopped successfully." - } - except APIError as e: - return { - "status": "error", - "exit_code": "APIError", - "output": str(e) - } - except Exception as e: - return { - "status": "error", - "exit_code": "Exception", - "output": str(e) - } - - def restart_container(self, container_name): - """ - Restart a container by its container name. - :param container_name: The name of the container. - :return: A standardized response with status, output, and exit_code. - """ - - filters = {"name": container_name} - - try: - for container in self.client.containers.list(filters=filters): - container.restart() - return { - "status": "success", - "exit_code": "0", - "output": f"Container '{container_name}' restarted successfully." - } - except APIError as e: - return { - "status": "error", - "exit_code": "APIError", - "output": str(e) - } - except Exception as e: - return { - "status": "error", - "exit_code": "Exception", - "output": str(e) - } diff --git a/data/Dockerfiles/controller/mailcow-adm/modules/Dovecot.py b/data/Dockerfiles/controller/mailcow-adm/modules/Dovecot.py deleted file mode 100644 index 818345404..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/modules/Dovecot.py +++ /dev/null @@ -1,206 +0,0 @@ -import os - -from modules.Docker import Docker - -class Dovecot: - def __init__(self): - self.docker = Docker() - - def decryptMaildir(self, source_dir="/var/vmail/", output_dir=None): - """ - Decrypt files in /var/vmail using doveadm if they are encrypted. - :param output_dir: Directory inside the Dovecot container to store decrypted files, Default overwrite. - """ - private_key = "/mail_crypt/ecprivkey.pem" - public_key = "/mail_crypt/ecpubkey.pem" - - if output_dir: - # Ensure the output directory exists inside the container - mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c 'mkdir -p {output_dir} && chown vmail:vmail {output_dir}'") - if mkdir_result.get("status") != "success": - print(f"Error creating output directory: {mkdir_result.get('output')}") - return - - find_command = [ - "find", source_dir, "-type", "f", "-regextype", "egrep", "-regex", ".*S=.*W=.*" - ] - - try: - find_result = self.docker.exec_command("dovecot-mailcow", " ".join(find_command)) - if find_result.get("status") != "success": - print(f"Error finding files: {find_result.get('output')}") - return - - files = find_result.get("output", "").splitlines() - - for file in files: - head_command = f"head -c7 {file}" - head_result = self.docker.exec_command("dovecot-mailcow", head_command) - if head_result.get("status") == "success" and head_result.get("output", "").strip() == "CRYPTED": - if output_dir: - # Preserve the directory structure in the output directory - relative_path = os.path.relpath(file, source_dir) - output_file = os.path.join(output_dir, relative_path) - current_path = output_dir - for part in os.path.dirname(relative_path).split(os.sep): - current_path = os.path.join(current_path, part) - mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c '[ ! -d {current_path} ] && mkdir {current_path} && chown vmail:vmail {current_path}'") - if mkdir_result.get("status") != "success": - print(f"Error creating directory {current_path}: {mkdir_result.get('output')}") - continue - else: - # Overwrite the original file - output_file = file - - decrypt_command = ( - f"bash -c 'doveadm fs get compress lz4:1:crypt:private_key_path={private_key}:public_key_path={public_key}:posix:prefix=/ {file} > {output_file}'" - ) - - decrypt_result = self.docker.exec_command("dovecot-mailcow", decrypt_command) - if decrypt_result.get("status") == "success": - print(f"Decrypted {file}") - - # Verify the file size and set permissions - size_check_command = f"bash -c '[ -s {output_file} ] && chmod 600 {output_file} && chown vmail:vmail {output_file} || rm -f {output_file}'" - size_check_result = self.docker.exec_command("dovecot-mailcow", size_check_command) - if size_check_result.get("status") != "success": - print(f"Error setting permissions for {output_file}: {size_check_result.get('output')}\n") - - except Exception as e: - print(f"Error during decryption: {e}") - - return "Done" - - def encryptMaildir(self, source_dir="/var/vmail/", output_dir=None): - """ - Encrypt files in /var/vmail using doveadm if they are not already encrypted. - :param source_dir: Directory inside the Dovecot container to encrypt files. - :param output_dir: Directory inside the Dovecot container to store encrypted files, Default overwrite. - """ - private_key = "/mail_crypt/ecprivkey.pem" - public_key = "/mail_crypt/ecpubkey.pem" - - if output_dir: - # Ensure the output directory exists inside the container - mkdir_result = self.docker.exec_command("dovecot-mailcow", f"mkdir -p {output_dir}") - if mkdir_result.get("status") != "success": - print(f"Error creating output directory: {mkdir_result.get('output')}") - return - - find_command = [ - "find", source_dir, "-type", "f", "-regextype", "egrep", "-regex", ".*S=.*W=.*" - ] - - try: - find_result = self.docker.exec_command("dovecot-mailcow", " ".join(find_command)) - if find_result.get("status") != "success": - print(f"Error finding files: {find_result.get('output')}") - return - - files = find_result.get("output", "").splitlines() - - for file in files: - head_command = f"head -c7 {file}" - head_result = self.docker.exec_command("dovecot-mailcow", head_command) - if head_result.get("status") == "success" and head_result.get("output", "").strip() != "CRYPTED": - if output_dir: - # Preserve the directory structure in the output directory - relative_path = os.path.relpath(file, source_dir) - output_file = os.path.join(output_dir, relative_path) - current_path = output_dir - for part in os.path.dirname(relative_path).split(os.sep): - current_path = os.path.join(current_path, part) - mkdir_result = self.docker.exec_command("dovecot-mailcow", f"bash -c '[ ! -d {current_path} ] && mkdir {current_path} && chown vmail:vmail {current_path}'") - if mkdir_result.get("status") != "success": - print(f"Error creating directory {current_path}: {mkdir_result.get('output')}") - continue - else: - # Overwrite the original file - output_file = file - - encrypt_command = ( - f"bash -c 'doveadm fs put crypt private_key_path={private_key}:public_key_path={public_key}:posix:prefix=/ {file} {output_file}'" - ) - - encrypt_result = self.docker.exec_command("dovecot-mailcow", encrypt_command) - if encrypt_result.get("status") == "success": - print(f"Encrypted {file}") - - # Set permissions - permissions_command = f"bash -c 'chmod 600 {output_file} && chown 5000:5000 {output_file}'" - permissions_result = self.docker.exec_command("dovecot-mailcow", permissions_command) - if permissions_result.get("status") != "success": - print(f"Error setting permissions for {output_file}: {permissions_result.get('output')}\n") - - except Exception as e: - print(f"Error during encryption: {e}") - - return "Done" - - def listDeletedMaildirs(self, source_dir="/var/vmail/_garbage"): - """ - List deleted maildirs in the specified garbage directory. - :param source_dir: Directory to search for deleted maildirs. - :return: List of maildirs. - """ - list_command = ["bash", "-c", f"ls -la {source_dir}"] - - try: - result = self.docker.exec_command("dovecot-mailcow", list_command) - if result.get("status") != "success": - print(f"Error listing deleted maildirs: {result.get('output')}") - return [] - - lines = result.get("output", "").splitlines() - maildirs = {} - - for idx, line in enumerate(lines): - parts = line.split() - if "_" in line: - folder_name = parts[-1] - time, maildir = folder_name.split("_", 1) - - if maildir.endswith("_index"): - main_item = maildir[:-6] - if main_item in maildirs: - maildirs[main_item]["has_index"] = True - else: - maildirs[maildir] = {"item": idx, "time": time, "name": maildir, "has_index": False} - - return list(maildirs.values()) - - except Exception as e: - print(f"Error during listing deleted maildirs: {e}") - return [] - - def restoreMaildir(self, username, item, source_dir="/var/vmail/_garbage"): - """ - Restore a maildir item for a specific user from the deleted maildirs. - :param username: Username to restore the item to. - :param item: Item to restore (e.g., mailbox, folder). - :param source_dir: Directory containing deleted maildirs. - :return: Response from Dovecot. - """ - username_splitted = username.split("@") - maildirs = self.listDeletedMaildirs() - - maildir = None - for mdir in maildirs: - if mdir["item"] == int(item): - maildir = mdir - break - if not maildir: - return {"status": "error", "message": "Maildir not found."} - - restore_command = f"mv {source_dir}/{maildir['time']}_{maildir['name']} /var/vmail/{username_splitted[1]}/{username_splitted[0]}" - restore_index_command = f"mv {source_dir}/{maildir['time']}_{maildir['name']}_index /var/vmail_index/{username}" - - result = self.docker.exec_command("dovecot-mailcow", ["bash", "-c", restore_command]) - if result.get("status") != "success": - return {"status": "error", "message": "Failed to restore maildir."} - - result = self.docker.exec_command("dovecot-mailcow", ["bash", "-c", restore_index_command]) - if result.get("status") != "success": - return {"status": "error", "message": "Failed to restore maildir index."} - - return "Done" diff --git a/data/Dockerfiles/controller/mailcow-adm/modules/Mailcow.py b/data/Dockerfiles/controller/mailcow-adm/modules/Mailcow.py deleted file mode 100644 index 2f006cd11..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/modules/Mailcow.py +++ /dev/null @@ -1,457 +0,0 @@ -import requests -import urllib3 -import sys -import os -import subprocess -import tempfile -import mysql.connector -from contextlib import contextmanager -from datetime import datetime -from modules.Docker import Docker - - -class Mailcow: - def __init__(self): - self.apiUrl = "/api/v1" - self.ignore_ssl_errors = True - - self.baseUrl = f"https://{os.getenv('IPv4_NETWORK', '172.22.1')}.247:{os.getenv('HTTPS_PORT', '443')}" - self.host = os.getenv("MAILCOW_HOSTNAME", "") - self.apiKey = "" - if self.ignore_ssl_errors: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - self.db_config = { - 'user': os.getenv('DBUSER'), - 'password': os.getenv('DBPASS'), - 'database': os.getenv('DBNAME'), - 'unix_socket': '/var/run/mysqld/mysqld.sock', - } - - self.docker = Docker() - - - # API Functions - def addDomain(self, domain): - """ - Add a domain to the mailcow instance. - :param domain: Dictionary containing domain details. - :return: Response from the mailcow API. - """ - - return self.post('/add/domain', domain) - - def addMailbox(self, mailbox): - """ - Add a mailbox to the mailcow instance. - :param mailbox: Dictionary containing mailbox details. - :return: Response from the mailcow API. - """ - - return self.post('/add/mailbox', mailbox) - - def addAlias(self, alias): - """ - Add an alias to the mailcow instance. - :param alias: Dictionary containing alias details. - :return: Response from the mailcow API. - """ - - return self.post('/add/alias', alias) - - def addSyncjob(self, syncjob): - """ - Add a sync job to the mailcow instance. - :param syncjob: Dictionary containing sync job details. - :return: Response from the mailcow API. - """ - - return self.post('/add/syncjob', syncjob) - - def addDomainadmin(self, domainadmin): - """ - Add a domain admin to the mailcow instance. - :param domainadmin: Dictionary containing domain admin details. - :return: Response from the mailcow API. - """ - - return self.post('/add/domain-admin', domainadmin) - - def deleteDomain(self, domain): - """ - Delete a domain from the mailcow instance. - :param domain: Name of the domain to delete. - :return: Response from the mailcow API. - """ - - items = [domain] - return self.post('/delete/domain', items) - - def deleteAlias(self, id): - """ - Delete an alias from the mailcow instance. - :param id: ID of the alias to delete. - :return: Response from the mailcow API. - """ - - items = [id] - return self.post('/delete/alias', items) - - def deleteSyncjob(self, id): - """ - Delete a sync job from the mailcow instance. - :param id: ID of the sync job to delete. - :return: Response from the mailcow API. - """ - - items = [id] - return self.post('/delete/syncjob', items) - - def deleteMailbox(self, mailbox): - """ - Delete a mailbox from the mailcow instance. - :param mailbox: Name of the mailbox to delete. - :return: Response from the mailcow API. - """ - - items = [mailbox] - return self.post('/delete/mailbox', items) - - def deleteDomainadmin(self, username): - """ - Delete a domain admin from the mailcow instance. - :param username: Username of the domain admin to delete. - :return: Response from the mailcow API. - """ - - items = [username] - return self.post('/delete/domain-admin', items) - - def post(self, endpoint, data): - """ - Make a POST request to the mailcow API. - :param endpoint: The API endpoint to post to. - :param data: Data to be sent in the POST request. - :return: Response from the mailcow API. - """ - - url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}" - headers = { - "Content-Type": "application/json", - "Host": self.host - } - if self.apiKey: - headers["X-Api-Key"] = self.apiKey - response = requests.post( - url, - json=data, - headers=headers, - verify=not self.ignore_ssl_errors - ) - response.raise_for_status() - return response.json() - - def getDomain(self, domain): - """ - Get a domain from the mailcow instance. - :param domain: Name of the domain to get. - :return: Response from the mailcow API. - """ - - return self.get(f'/get/domain/{domain}') - - def getMailbox(self, username): - """ - Get a mailbox from the mailcow instance. - :param mailbox: Dictionary containing mailbox details (e.g. {"username": "user@example.com"}) - :return: Response from the mailcow API. - """ - return self.get(f'/get/mailbox/{username}') - - def getAlias(self, id): - """ - Get an alias from the mailcow instance. - :param alias: Dictionary containing alias details (e.g. {"address": "alias@example.com"}) - :return: Response from the mailcow API. - """ - return self.get(f'/get/alias/{id}') - - def getSyncjob(self, id): - """ - Get a sync job from the mailcow instance. - :param syncjob: Dictionary containing sync job details (e.g. {"id": "123"}) - :return: Response from the mailcow API. - """ - return self.get(f'/get/syncjobs/{id}') - - def getDomainadmin(self, username): - """ - Get a domain admin from the mailcow instance. - :param username: Username of the domain admin to get. - :return: Response from the mailcow API. - """ - return self.get(f'/get/domain-admin/{username}') - - def getStatusVersion(self): - """ - Get the version of the mailcow instance. - :return: Response from the mailcow API. - """ - return self.get('/get/status/version') - - def getStatusVmail(self): - """ - Get the vmail status from the mailcow instance. - :return: Response from the mailcow API. - """ - return self.get('/get/status/vmail') - - def getStatusContainers(self): - """ - Get the status of containers from the mailcow instance. - :return: Response from the mailcow API. - """ - return self.get('/get/status/containers') - - def get(self, endpoint, params=None): - """ - Make a GET request to the mailcow API. - :param endpoint: The API endpoint to get from. - :param params: Parameters to be sent in the GET request. - :return: Response from the mailcow API. - """ - - url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}" - headers = { - "Content-Type": "application/json", - "Host": self.host - } - if self.apiKey: - headers["X-Api-Key"] = self.apiKey - response = requests.get( - url, - params=params, - headers=headers, - verify=not self.ignore_ssl_errors - ) - response.raise_for_status() - return response.json() - - def editDomain(self, domain, attributes): - """ - Edit an existing domain in the mailcow instance. - :param domain: Name of the domain to edit - :param attributes: Dictionary containing the new domain attributes. - """ - - items = [domain] - return self.edit('/edit/domain', items, attributes) - - def editMailbox(self, mailbox, attributes): - """ - Edit an existing mailbox in the mailcow instance. - :param mailbox: Name of the mailbox to edit - :param attributes: Dictionary containing the new mailbox attributes. - """ - - items = [mailbox] - return self.edit('/edit/mailbox', items, attributes) - - def editAlias(self, alias, attributes): - """ - Edit an existing alias in the mailcow instance. - :param alias: Name of the alias to edit - :param attributes: Dictionary containing the new alias attributes. - """ - - items = [alias] - return self.edit('/edit/alias', items, attributes) - - def editSyncjob(self, syncjob, attributes): - """ - Edit an existing sync job in the mailcow instance. - :param syncjob: Name of the sync job to edit - :param attributes: Dictionary containing the new sync job attributes. - """ - - items = [syncjob] - return self.edit('/edit/syncjob', items, attributes) - - def editDomainadmin(self, username, attributes): - """ - Edit an existing domain admin in the mailcow instance. - :param username: Username of the domain admin to edit - :param attributes: Dictionary containing the new domain admin attributes. - """ - - items = [username] - return self.edit('/edit/domain-admin', items, attributes) - - def edit(self, endpoint, items, attributes): - """ - Make a POST request to edit items in the mailcow API. - :param items: List of items to edit. - :param attributes: Dictionary containing the new attributes for the items. - :return: Response from the mailcow API. - """ - - url = f"{self.baseUrl}{self.apiUrl}/{endpoint.lstrip('/')}" - headers = { - "Content-Type": "application/json", - "Host": self.host - } - if self.apiKey: - headers["X-Api-Key"] = self.apiKey - data = { - "items": items, - "attr": attributes - } - response = requests.post( - url, - json=data, - headers=headers, - verify=not self.ignore_ssl_errors - ) - response.raise_for_status() - return response.json() - - - # System Functions - def runSyncjob(self, id, force=False): - """ - Run a sync job. - :param id: ID of the sync job to run. - :return: Response from the imapsync script. - """ - - creds_path = "/app/sieve.creds" - - conn = mysql.connector.connect(**self.db_config) - cursor = conn.cursor(dictionary=True) - - with open(creds_path, 'r') as file: - master_user, master_pass = file.read().strip().split(':') - - query = ("SELECT * FROM imapsync WHERE id = %s") - cursor.execute(query, (id,)) - - success = False - syncjob = cursor.fetchone() - if not syncjob: - cursor.close() - conn.close() - return f"Sync job with ID {id} not found." - if syncjob['active'] == 0 and not force: - cursor.close() - conn.close() - return f"Sync job with ID {id} is not active." - - enc1_flag = "--tls1" if syncjob['enc1'] == "TLS" else "--ssl1" if syncjob['enc1'] == "SSL" else None - - - passfile1_path = f"/tmp/passfile1_{id}.txt" - passfile2_path = f"/tmp/passfile2_{id}.txt" - passfile1_cmd = [ - "sh", "-c", - f"echo {syncjob['password1']} > {passfile1_path}" - ] - passfile2_cmd = [ - "sh", "-c", - f"echo {master_pass} > {passfile2_path}" - ] - - self.docker.exec_command("dovecot-mailcow", passfile1_cmd) - self.docker.exec_command("dovecot-mailcow", passfile2_cmd) - - imapsync_cmd = [ - "/usr/local/bin/imapsync", - "--tmpdir", "/tmp", - "--nofoldersizes", - "--addheader" - ] - - if int(syncjob['timeout1']) > 0: - imapsync_cmd.extend(['--timeout1', str(syncjob['timeout1'])]) - if int(syncjob['timeout2']) > 0: - imapsync_cmd.extend(['--timeout2', str(syncjob['timeout2'])]) - if syncjob['exclude']: - imapsync_cmd.extend(['--exclude', syncjob['exclude']]) - if syncjob['subfolder2']: - imapsync_cmd.extend(['--subfolder2', syncjob['subfolder2']]) - if int(syncjob['maxage']) > 0: - imapsync_cmd.extend(['--maxage', str(syncjob['maxage'])]) - if int(syncjob['maxbytespersecond']) > 0: - imapsync_cmd.extend(['--maxbytespersecond', str(syncjob['maxbytespersecond'])]) - if int(syncjob['delete2duplicates']) == 1: - imapsync_cmd.append("--delete2duplicates") - if int(syncjob['subscribeall']) == 1: - imapsync_cmd.append("--subscribeall") - if int(syncjob['delete1']) == 1: - imapsync_cmd.append("--delete") - if int(syncjob['delete2']) == 1: - imapsync_cmd.append("--delete2") - if int(syncjob['automap']) == 1: - imapsync_cmd.append("--automap") - if int(syncjob['skipcrossduplicates']) == 1: - imapsync_cmd.append("--skipcrossduplicates") - if enc1_flag: - imapsync_cmd.append(enc1_flag) - - imapsync_cmd.extend([ - "--host1", syncjob['host1'], - "--user1", syncjob['user1'], - "--passfile1", passfile1_path, - "--port1", str(syncjob['port1']), - "--host2", "localhost", - "--user2", f"{syncjob['user2']}*{master_user}", - "--passfile2", passfile2_path - ]) - - if syncjob['dry'] == 1: - imapsync_cmd.append("--dry") - - imapsync_cmd.extend([ - "--no-modulesversion", - "--noreleasecheck" - ]) - - try: - cursor.execute("UPDATE imapsync SET is_running = 1, success = NULL, exit_status = NULL WHERE id = %s", (id,)) - conn.commit() - - result = self.docker.exec_command("dovecot-mailcow", imapsync_cmd) - print(result) - - success = result['status'] == "success" and result['exit_code'] == 0 - cursor.execute( - "UPDATE imapsync SET returned_text = %s, success = %s, exit_status = %s WHERE id = %s", - (result['output'], int(success), result['exit_code'], id) - ) - conn.commit() - - except Exception as e: - cursor.execute( - "UPDATE imapsync SET returned_text = %s, success = 0 WHERE id = %s", - (str(e), id) - ) - conn.commit() - - finally: - cursor.execute("UPDATE imapsync SET last_run = NOW(), is_running = 0 WHERE id = %s", (id,)) - conn.commit() - - delete_passfile1_cmd = [ - "sh", "-c", - f"rm -f {passfile1_path}" - ] - delete_passfile2_cmd = [ - "sh", "-c", - f"rm -f {passfile2_path}" - ] - self.docker.exec_command("dovecot-mailcow", delete_passfile1_cmd) - self.docker.exec_command("dovecot-mailcow", delete_passfile2_cmd) - - cursor.close() - conn.close() - - return "Sync job completed successfully." if success else "Sync job failed." diff --git a/data/Dockerfiles/controller/mailcow-adm/modules/Mailer.py b/data/Dockerfiles/controller/mailcow-adm/modules/Mailer.py deleted file mode 100644 index 6bb73d5d1..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/modules/Mailer.py +++ /dev/null @@ -1,64 +0,0 @@ -import smtplib -import json -import os -from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from jinja2 import Environment, BaseLoader - -class Mailer: - def __init__(self, smtp_host, smtp_port, username, password, use_tls=True): - self.smtp_host = smtp_host - self.smtp_port = smtp_port - self.username = username - self.password = password - self.use_tls = use_tls - self.server = None - self.env = Environment(loader=BaseLoader()) - - def connect(self): - print("Connecting to the SMTP server...") - self.server = smtplib.SMTP(self.smtp_host, self.smtp_port) - if self.use_tls: - self.server.starttls() - print("TLS activated!") - if self.username and self.password: - self.server.login(self.username, self.password) - print("Authenticated!") - - def disconnect(self): - if self.server: - try: - if self.server.sock: - self.server.quit() - except smtplib.SMTPServerDisconnected: - pass - finally: - self.server = None - - def render_inline_template(self, template_string, context): - template = self.env.from_string(template_string) - return template.render(context) - - def send_mail(self, subject, from_addr, to_addrs, template, context = {}): - try: - if template == "": - print("Cannot send email, template is empty!") - return "Failed: Template is empty." - - body = self.render_inline_template(template, context) - - msg = MIMEMultipart() - msg['From'] = from_addr - msg['To'] = ', '.join(to_addrs) if isinstance(to_addrs, list) else to_addrs - msg['Subject'] = subject - msg.attach(MIMEText(body, 'html')) - - self.connect() - self.server.sendmail(from_addr, to_addrs, msg.as_string()) - self.disconnect() - return f"Success: Email sent to {msg['To']}" - except Exception as e: - print(f"Error during send_mail: {type(e).__name__}: {e}") - return f"Failed: {type(e).__name__}: {e}" - finally: - self.disconnect() diff --git a/data/Dockerfiles/controller/mailcow-adm/modules/Reader.py b/data/Dockerfiles/controller/mailcow-adm/modules/Reader.py deleted file mode 100644 index e2fafc24f..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/modules/Reader.py +++ /dev/null @@ -1,51 +0,0 @@ -from jinja2 import Environment, Template -import csv - -def split_at(value, sep, idx): - try: - return value.split(sep)[idx] - except Exception: - return '' - -class Reader: - """ - Reader class to handle reading and processing of CSV and JSON files for mailcow. - """ - - def __init__(self): - pass - - def read_csv(self, file_path, delimiter=',', encoding='iso-8859-1'): - """ - Read a CSV file and return a list of dictionaries. - Each dictionary represents a row in the CSV file. - :param file_path: Path to the CSV file. - :param delimiter: Delimiter used in the CSV file (default: ','). - """ - with open(file_path, mode='r', encoding=encoding) as file: - reader = csv.DictReader(file, delimiter=delimiter) - reader.fieldnames = [h.replace(" ", "_") if h else h for h in reader.fieldnames] - return [row for row in reader] - - def map_csv_data(self, data, mapping_file_path, encoding='iso-8859-1'): - """ - Map CSV data to a specific structure based on the provided Jinja2 template file. - :param data: List of dictionaries representing CSV rows. - :param mapping_file_path: Path to the Jinja2 template file. - :return: List of dictionaries with mapped data. - """ - with open(mapping_file_path, 'r', encoding=encoding) as tpl_file: - template_content = tpl_file.read() - env = Environment() - env.filters['split_at'] = split_at - template = env.from_string(template_content) - - mapped_data = [] - for row in data: - rendered = template.render(**row) - try: - mapped_row = eval(rendered) - except Exception: - mapped_row = rendered - mapped_data.append(mapped_row) - return mapped_data \ No newline at end of file diff --git a/data/Dockerfiles/controller/mailcow-adm/modules/Sogo.py b/data/Dockerfiles/controller/mailcow-adm/modules/Sogo.py deleted file mode 100644 index 56e245e0a..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/modules/Sogo.py +++ /dev/null @@ -1,512 +0,0 @@ -import requests -import urllib3 -import os -from uuid import uuid4 -from collections import defaultdict - - -class Sogo: - def __init__(self, username, password=""): - self.apiUrl = "/SOGo/so" - self.davUrl = "/SOGo/dav" - self.ignore_ssl_errors = True - - self.baseUrl = f"https://{os.getenv('IPv4_NETWORK', '172.22.1')}.247:{os.getenv('HTTPS_PORT', '443')}" - self.host = os.getenv("MAILCOW_HOSTNAME", "") - if self.ignore_ssl_errors: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - self.username = username - self.password = password - - def addCalendar(self, calendar_name): - """ - Add a new calendar to the sogo instance. - :param calendar_name: Name of the calendar to be created - :return: Response from the sogo API. - """ - - res = self.post(f"/{self.username}/Calendar/createFolder", { - "name": calendar_name - }) - try: - return res.json() - except ValueError: - return res.text - - def getCalendarIdByName(self, calendar_name): - """ - Get the calendar ID by its name. - :param calendar_name: Name of the calendar to find - :return: Calendar ID if found, otherwise None. - """ - - res = self.get(f"/{self.username}/Calendar/calendarslist") - try: - for calendar in res.json()["calendars"]: - if calendar['name'] == calendar_name: - return calendar['id'] - except ValueError: - return None - return None - - def getCalendar(self): - """ - Get calendar list. - :return: Response from SOGo API. - """ - - res = self.get(f"/{self.username}/Calendar/calendarslist") - try: - return res.json() - except ValueError: - return res.text - - def deleteCalendar(self, calendar_id): - """ - Delete a calendar. - :param calendar_id: ID of the calendar to be deleted - :return: Response from SOGo API. - """ - res = self.get(f"/{self.username}/Calendar/{calendar_id}/delete") - return res.status_code == 204 - - def importCalendar(self, calendar_name, ics_file): - """ - Import a calendar from an ICS file. - :param calendar_name: Name of the calendar to import into - :param ics_file: Path to the ICS file to import - :return: Response from SOGo API. - """ - - try: - with open(ics_file, "rb") as f: - pass - except Exception as e: - print(f"Could not open ICS file '{ics_file}': {e}") - return {"status": "error", "message": str(e)} - - new_calendar = self.addCalendar(calendar_name) - selected_calendar = new_calendar.json()["id"] - - url = f"{self.baseUrl}{self.apiUrl}/{self.username}/Calendar/{selected_calendar}/import" - auth = (self.username, self.password) - with open(ics_file, "rb") as f: - files = {'icsFile': (ics_file, f, 'text/calendar')} - res = requests.post( - url, - files=files, - auth=auth, - verify=not self.ignore_ssl_errors - ) - try: - return res.json() - except ValueError: - return res.text - - return None - - def setCalendarACL(self, calendar_id, sharee_email, acl="r", subscribe=False): - """ - Set CalDAV calendar permissions for a user (sharee). - :param calendar_id: ID of the calendar to share - :param sharee_email: Email of the user to share with - :param acl: "w" for write, "r" for read-only or combination "rw" for read-write - :param subscribe: True will scubscribe the sharee to the calendar - :return: None - """ - - # Access rights - if acl == "" or len(acl) > 2: - return "Invalid acl level specified. Use 'w', 'r' or combinations like 'rw'." - rights = [{ - "c_email": sharee_email, - "uid": sharee_email, - "userClass": "normal-user", - "rights": { - "Public": "None", - "Private": "None", - "Confidential": "None", - "canCreateObjects": 0, - "canEraseObjects": 0 - } - }] - if "w" in acl: - rights[0]["rights"]["canCreateObjects"] = 1 - rights[0]["rights"]["canEraseObjects"] = 1 - if "r" in acl: - rights[0]["rights"]["Public"] = "Viewer" - rights[0]["rights"]["Private"] = "Viewer" - rights[0]["rights"]["Confidential"] = "Viewer" - - r_add = self.get(f"/{self.username}/Calendar/{calendar_id}/addUserInAcls?uid={sharee_email}") - if r_add.status_code < 200 or r_add.status_code > 299: - try: - return r_add.json() - except ValueError: - return r_add.text - - r_save = self.post(f"/{self.username}/Calendar/{calendar_id}/saveUserRights", rights) - if r_save.status_code < 200 or r_save.status_code > 299: - try: - return r_save.json() - except ValueError: - return r_save.text - - if subscribe: - r_subscribe = self.get(f"/{self.username}/Calendar/{calendar_id}/subscribeUsers?uids={sharee_email}") - if r_subscribe.status_code < 200 or r_subscribe.status_code > 299: - try: - return r_subscribe.json() - except ValueError: - return r_subscribe.text - - return r_save.status_code == 200 - - def getCalendarACL(self, calendar_id): - """ - Get CalDAV calendar permissions for a user (sharee). - :param calendar_id: ID of the calendar to get ACL from - :return: Response from SOGo API. - """ - - res = self.get(f"/{self.username}/Calendar/{calendar_id}/acls") - try: - return res.json() - except ValueError: - return res.text - - def deleteCalendarACL(self, calendar_id, sharee_email): - """ - Delete a calendar ACL for a user (sharee). - :param calendar_id: ID of the calendar to delete ACL from - :param sharee_email: Email of the user whose ACL to delete - :return: Response from SOGo API. - """ - - res = self.get(f"/{self.username}/Calendar/{calendar_id}/removeUserFromAcls?uid={sharee_email}") - return res.status_code == 204 - - def addAddressbook(self, addressbook_name): - """ - Add a new addressbook to the sogo instance. - :param addressbook_name: Name of the addressbook to be created - :return: Response from the sogo API. - """ - - res = self.post(f"/{self.username}/Contacts/createFolder", { - "name": addressbook_name - }) - try: - return res.json() - except ValueError: - return res.text - - def getAddressbookIdByName(self, addressbook_name): - """ - Get the addressbook ID by its name. - :param addressbook_name: Name of the addressbook to find - :return: Addressbook ID if found, otherwise None. - """ - - res = self.get(f"/{self.username}/Contacts/addressbooksList") - try: - for addressbook in res.json()["addressbooks"]: - if addressbook['name'] == addressbook_name: - return addressbook['id'] - except ValueError: - return None - return None - - def deleteAddressbook(self, addressbook_id): - """ - Delete an addressbook. - :param addressbook_id: ID of the addressbook to be deleted - :return: Response from SOGo API. - """ - - res = self.get(f"/{self.username}/Contacts/{addressbook_id}/delete") - return res.status_code == 204 - - def getAddressbookList(self): - """ - Get addressbook list. - :return: Response from SOGo API. - """ - - res = self.get(f"/{self.username}/Contacts/addressbooksList") - try: - return res.json() - except ValueError: - return res.text - - def setAddressbookACL(self, addressbook_id, sharee_email, acl="r", subscribe=False): - """ - Set CalDAV addressbook permissions for a user (sharee). - :param addressbook_id: ID of the addressbook to share - :param sharee_email: Email of the user to share with - :param acl: "w" for write, "r" for read-only or combination "rw" for read-write - :param subscribe: True will subscribe the sharee to the addressbook - :return: None - """ - - # Access rights - if acl == "" or len(acl) > 2: - print("Invalid acl level specified. Use 's', 'w', 'r' or combinations like 'rws'.") - return "Invalid acl level specified. Use 'w', 'r' or combinations like 'rw'." - rights = [{ - "c_email": sharee_email, - "uid": sharee_email, - "userClass": "normal-user", - "rights": { - "canCreateObjects": 0, - "canEditObjects": 0, - "canEraseObjects": 0, - "canViewObjects": 0, - } - }] - if "w" in acl: - rights[0]["rights"]["canCreateObjects"] = 1 - rights[0]["rights"]["canEditObjects"] = 1 - rights[0]["rights"]["canEraseObjects"] = 1 - if "r" in acl: - rights[0]["rights"]["canViewObjects"] = 1 - - r_add = self.get(f"/{self.username}/Contacts/{addressbook_id}/addUserInAcls?uid={sharee_email}") - if r_add.status_code < 200 or r_add.status_code > 299: - try: - return r_add.json() - except ValueError: - return r_add.text - - r_save = self.post(f"/{self.username}/Contacts/{addressbook_id}/saveUserRights", rights) - if r_save.status_code < 200 or r_save.status_code > 299: - try: - return r_save.json() - except ValueError: - return r_save.text - - if subscribe: - r_subscribe = self.get(f"/{self.username}/Contacts/{addressbook_id}/subscribeUsers?uids={sharee_email}") - if r_subscribe.status_code < 200 or r_subscribe.status_code > 299: - try: - return r_subscribe.json() - except ValueError: - return r_subscribe.text - - return r_save.status_code == 200 - - def getAddressbookACL(self, addressbook_id): - """ - Get CalDAV addressbook permissions for a user (sharee). - :param addressbook_id: ID of the addressbook to get ACL from - :return: Response from SOGo API. - """ - - res = self.get(f"/{self.username}/Contacts/{addressbook_id}/acls") - try: - return res.json() - except ValueError: - return res.text - - def deleteAddressbookACL(self, addressbook_id, sharee_email): - """ - Delete an addressbook ACL for a user (sharee). - :param addressbook_id: ID of the addressbook to delete ACL from - :param sharee_email: Email of the user whose ACL to delete - :return: Response from SOGo API. - """ - - res = self.get(f"/{self.username}/Contacts/{addressbook_id}/removeUserFromAcls?uid={sharee_email}") - return res.status_code == 204 - - def getAddressbookNewGuid(self, addressbook_id): - """ - Request a new GUID for a SOGo addressbook. - :param addressbook_id: ID of the addressbook - :return: JSON response from SOGo or None if not found - """ - res = self.get(f"/{self.username}/Contacts/{addressbook_id}/newguid") - try: - return res.json() - except ValueError: - return res.text - - def addAddressbookContact(self, addressbook_id, contact_name, contact_email): - """ - Save a vCard as a contact in the specified addressbook. - :param addressbook_id: ID of the addressbook - :param contact_name: Name of the contact - :param contact_email: Email of the contact - :return: JSON response from SOGo or None if not found - """ - vcard_id = self.getAddressbookNewGuid(addressbook_id) - contact_data = { - "id": vcard_id["id"], - "pid": vcard_id["pid"], - "c_cn": contact_name, - "emails": [{ - "type": "pref", - "value": contact_email - }], - "isNew": True, - "c_component": "vcard", - } - - endpoint = f"/{self.username}/Contacts/{addressbook_id}/{vcard_id['id']}/saveAsContact" - res = self.post(endpoint, contact_data) - try: - return res.json() - except ValueError: - return res.text - - def getAddressbookContacts(self, addressbook_id, contact_email=None): - """ - Get all contacts from the specified addressbook. - :param addressbook_id: ID of the addressbook - :return: JSON response with contacts or None if not found - """ - res = self.get(f"/{self.username}/Contacts/{addressbook_id}/view") - try: - res_json = res.json() - headers = res_json.get("headers", []) - if not headers or len(headers) < 2: - return [] - - field_names = headers[0] - contacts = [] - for row in headers[1:]: - contact = dict(zip(field_names, row)) - contacts.append(contact) - - if contact_email: - contact = {} - for c in contacts: - if c["c_mail"] == contact_email or c["c_cn"] == contact_email: - contact = c - break - return contact - - return contacts - except ValueError: - return res.text - - def addAddressbookContactList(self, addressbook_id, contact_name, contact_email=None): - """ - Add a new contact list to the addressbook. - :param addressbook_id: ID of the addressbook - :param contact_name: Name of the contact list - :param contact_email: Comma-separated emails to include in the list - :return: Response from SOGo API. - """ - gal_domain = self.username.split("@")[-1] - vlist_id = self.getAddressbookNewGuid(addressbook_id) - contact_emails = contact_email.split(",") if contact_email else [] - contacts = self.getAddressbookContacts(addressbook_id) - - refs = [] - for contact in contacts: - if contact['c_mail'] in contact_emails: - refs.append({ - "refs": [], - "categories": [], - "c_screenname": contact.get("c_screenname", ""), - "pid": contact.get("pid", vlist_id["pid"]), - "id": contact.get("id", ""), - "notes": [""], - "empty": " ", - "hasphoto": contact.get("hasphoto", 0), - "c_cn": contact.get("c_cn", ""), - "c_uid": contact.get("c_uid", None), - "containername": contact.get("containername", f"GAL {gal_domain}"), # or your addressbook name - "sourceid": contact.get("sourceid", gal_domain), - "c_component": contact.get("c_component", "vcard"), - "c_sn": contact.get("c_sn", ""), - "c_givenname": contact.get("c_givenname", ""), - "c_name": contact.get("c_name", contact.get("id", "")), - "c_telephonenumber": contact.get("c_telephonenumber", ""), - "fn": contact.get("fn", ""), - "c_mail": contact.get("c_mail", ""), - "emails": contact.get("emails", []), - "c_o": contact.get("c_o", ""), - "reference": contact.get("id", ""), - "birthday": contact.get("birthday", "") - }) - - contact_data = { - "refs": refs, - "categories": [], - "c_screenname": None, - "pid": vlist_id["pid"], - "c_component": "vlist", - "notes": [""], - "empty": " ", - "isNew": True, - "id": vlist_id["id"], - "c_cn": contact_name, - "birthday": "" - } - - endpoint = f"/{self.username}/Contacts/{addressbook_id}/{vlist_id['id']}/saveAsList" - res = self.post(endpoint, contact_data) - try: - return res.json() - except ValueError: - return res.text - - def deleteAddressbookItem(self, addressbook_id, contact_name): - """ - Delete an addressbook item by its ID. - :param addressbook_id: ID of the addressbook item to delete - :param contact_name: Name of the contact to delete - :return: Response from SOGo API. - """ - res = self.getAddressbookContacts(addressbook_id, contact_name) - - if "id" not in res: - print(f"Contact '{contact_name}' not found in addressbook '{addressbook_id}'.") - return None - res = self.post(f"/{self.username}/Contacts/{addressbook_id}/batchDelete", { - "uids": [res["id"]], - }) - return res.status_code == 204 - - def get(self, endpoint, params=None): - """ - Make a GET request to the mailcow API. - :param endpoint: The API endpoint to get. - :param params: Optional parameters for the GET request. - :return: Response from the mailcow API. - """ - url = f"{self.baseUrl}{self.apiUrl}{endpoint}" - auth = (self.username, self.password) - headers = {"Host": self.host} - - response = requests.get( - url, - params=params, - auth=auth, - headers=headers, - verify=not self.ignore_ssl_errors - ) - return response - - def post(self, endpoint, data): - """ - Make a POST request to the mailcow API. - :param endpoint: The API endpoint to post to. - :param data: Data to be sent in the POST request. - :return: Response from the mailcow API. - """ - url = f"{self.baseUrl}{self.apiUrl}{endpoint}" - auth = (self.username, self.password) - headers = {"Host": self.host} - - response = requests.post( - url, - json=data, - auth=auth, - headers=headers, - verify=not self.ignore_ssl_errors - ) - return response - diff --git a/data/Dockerfiles/controller/mailcow-adm/modules/Utils.py b/data/Dockerfiles/controller/mailcow-adm/modules/Utils.py deleted file mode 100644 index 69ea39bd4..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/modules/Utils.py +++ /dev/null @@ -1,37 +0,0 @@ -import json -import random -import string - - -class Utils: - def __init(self): - pass - - def normalize_email(self, email): - replacements = { - "ä": "ae", "ö": "oe", "ü": "ue", "ß": "ss", - "Ä": "Ae", "Ö": "Oe", "Ü": "Ue" - } - for orig, repl in replacements.items(): - email = email.replace(orig, repl) - return email - - def generate_password(self, length=8): - chars = string.ascii_letters + string.digits - return ''.join(random.choices(chars, k=length)) - - def pprint(self, data=""): - """ - Pretty print a dictionary, list, or text. - If data is a text containing JSON, it will be printed in a formatted way. - """ - if isinstance(data, (dict, list)): - print(json.dumps(data, indent=2, ensure_ascii=False)) - elif isinstance(data, str): - try: - json_data = json.loads(data) - print(json.dumps(json_data, indent=2, ensure_ascii=False)) - except json.JSONDecodeError: - print(data) - else: - print(data) diff --git a/data/Dockerfiles/controller/mailcow-adm/modules/__init__.py b/data/Dockerfiles/controller/mailcow-adm/modules/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/data/Dockerfiles/controller/mailcow-adm/requirements.txt b/data/Dockerfiles/controller/mailcow-adm/requirements.txt deleted file mode 100644 index 2fa87f190..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -jinja2 -requests -mysql-connector-python -pytest diff --git a/data/Dockerfiles/controller/mailcow-adm/tests/test_AliasModel.py b/data/Dockerfiles/controller/mailcow-adm/tests/test_AliasModel.py deleted file mode 100644 index 263adf478..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/tests/test_AliasModel.py +++ /dev/null @@ -1,94 +0,0 @@ -import pytest -import json -import sys -import os - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) -from models.DomainModel import DomainModel -from models.AliasModel import AliasModel - - -def test_model(): - # Generate random alias - random_alias = f"alias_test{os.urandom(4).hex()}@mailcow.local" - - # Create an instance of AliasModel - model = AliasModel( - address=random_alias, - goto="test@mailcow.local,test2@mailcow.local" - ) - - # Test the parser_command attribute - assert model.parser_command == "alias", "Parser command should be 'alias'" - - # add Domain for testing - domain_model = DomainModel(domain="mailcow.local") - domain_model.add() - - # 1. Alias add tests, should success - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[0]['type'] == "success", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[0]['msg'][0] == "alias_added", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'alias_added'\n{json.dumps(r_add, indent=2)}" - - # Assign created alias ID for further tests - model.id = r_add[0]['msg'][2] - - # 2. Alias add tests, should fail because the alias already exists - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[0]['msg'][0] == "is_alias_or_mailbox", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'is_alias_or_mailbox'\n{json.dumps(r_add, indent=2)}" - - # 3. Alias get tests - r_get = model.get() - assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}" - assert "domain" in r_get, f"'domain' key missing in response: {json.dumps(r_get, indent=2)}" - assert "goto" in r_get, f"'goto' key missing in response: {json.dumps(r_get, indent=2)}" - assert "address" in r_get, f"'address' key missing in response: {json.dumps(r_get, indent=2)}" - assert r_get['domain'] == model.address.split("@")[1], f"Wrong 'domain' received: {r_get['domain']}, expected: {model.address.split('@')[1]}\n{json.dumps(r_get, indent=2)}" - assert r_get['goto'] == model.goto, f"Wrong 'goto' received: {r_get['goto']}, expected: {model.goto}\n{json.dumps(r_get, indent=2)}" - assert r_get['address'] == model.address, f"Wrong 'address' received: {r_get['address']}, expected: {model.address}\n{json.dumps(r_get, indent=2)}" - - # 4. Alias edit tests - model.goto = "test@mailcow.local" - model.active = 0 - r_edit = model.edit() - assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}" - assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}" - assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}" - assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['msg'][0] == "alias_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'alias_modified'\n{json.dumps(r_edit, indent=2)}" - - # 5. Alias delete tests - r_delete = model.delete() - assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}" - assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}" - assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}" - assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['msg'][0] == "alias_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'alias_removed'\n{json.dumps(r_delete, indent=2)}" - - # delete testing Domain - domain_model.delete() - - -if __name__ == "__main__": - print("Running AliasModel tests...") - test_model() - print("All tests passed!") diff --git a/data/Dockerfiles/controller/mailcow-adm/tests/test_BaseModel.py b/data/Dockerfiles/controller/mailcow-adm/tests/test_BaseModel.py deleted file mode 100644 index ac7bfdb9e..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/tests/test_BaseModel.py +++ /dev/null @@ -1,71 +0,0 @@ -import pytest -from models.BaseModel import BaseModel - - -class Args: - def __init__(self, **kwargs): - for key, value in kwargs.items(): - setattr(self, key, value) - - -def test_has_required_args(): - BaseModel.required_args = { - "test_object": [["arg1"], ["arg2", "arg3"]], - } - - # Test cases with Args object - args = Args(object="non_existent_object") - assert BaseModel.has_required_args(args) == False - - args = Args(object="test_object") - assert BaseModel.has_required_args(args) == False - - args = Args(object="test_object", arg1="value") - assert BaseModel.has_required_args(args) == True - - args = Args(object="test_object", arg2="value") - assert BaseModel.has_required_args(args) == False - - args = Args(object="test_object", arg3="value") - assert BaseModel.has_required_args(args) == False - - args = Args(object="test_object", arg2="value", arg3="value") - assert BaseModel.has_required_args(args) == True - - # Test cases with dict object - args = {"object": "non_existent_object"} - assert BaseModel.has_required_args(args) == False - - args = {"object": "test_object"} - assert BaseModel.has_required_args(args) == False - - args = {"object": "test_object", "arg1": "value"} - assert BaseModel.has_required_args(args) == True - - args = {"object": "test_object", "arg2": "value"} - assert BaseModel.has_required_args(args) == False - - args = {"object": "test_object", "arg3": "value"} - assert BaseModel.has_required_args(args) == False - - args = {"object": "test_object", "arg2": "value", "arg3": "value"} - assert BaseModel.has_required_args(args) == True - - - BaseModel.required_args = { - "test_object": [[]], - } - - # Test cases with Args object - args = Args(object="non_existent_object") - assert BaseModel.has_required_args(args) == False - - args = Args(object="test_object") - assert BaseModel.has_required_args(args) == True - - # Test cases with dict object - args = {"object": "non_existent_object"} - assert BaseModel.has_required_args(args) == False - - args = {"object": "test_object"} - assert BaseModel.has_required_args(args) == True diff --git a/data/Dockerfiles/controller/mailcow-adm/tests/test_DomainModel.py b/data/Dockerfiles/controller/mailcow-adm/tests/test_DomainModel.py deleted file mode 100644 index f52c234e9..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/tests/test_DomainModel.py +++ /dev/null @@ -1,74 +0,0 @@ -import pytest -import json -import sys -import os - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) -from models.DomainModel import DomainModel - - -def test_model(): - # Create an instance of DomainModel - model = DomainModel( - domain="mailcow.local", - ) - - # Test the parser_command attribute - assert model.parser_command == "domain", "Parser command should be 'domain'" - - # 1. Domain add tests, should success - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0 and len(r_add) >= 2, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[1], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[1]['type'] == "success", f"Wrong 'type' received: {r_add[1]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[1], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[1]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[1]['msg']) > 0 and len(r_add[1]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[1]['msg'][0] == "domain_added", f"Wrong 'msg' received: {r_add[1]['msg'][0]}, expected: 'domain_added'\n{json.dumps(r_add, indent=2)}" - - # 2. Domain add tests, should fail because the domain already exists - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[0]['msg'][0] == "domain_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'domain_exists'\n{json.dumps(r_add, indent=2)}" - - # 3. Domain get tests - r_get = model.get() - assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}" - assert "domain_name" in r_get, f"'domain_name' key missing in response: {json.dumps(r_get, indent=2)}" - assert r_get['domain_name'] == model.domain, f"Wrong 'domain_name' received: {r_get['domain_name']}, expected: {model.domain}\n{json.dumps(r_get, indent=2)}" - - # 4. Domain edit tests - model.active = 0 - r_edit = model.edit() - assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}" - assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}" - assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}" - assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['msg'][0] == "domain_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'domain_modified'\n{json.dumps(r_edit, indent=2)}" - - # 5. Domain delete tests - r_delete = model.delete() - assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}" - assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}" - assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}" - assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['msg'][0] == "domain_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'domain_removed'\n{json.dumps(r_delete, indent=2)}" - - -if __name__ == "__main__": - print("Running DomainModel tests...") - test_model() - print("All tests passed!") diff --git a/data/Dockerfiles/controller/mailcow-adm/tests/test_DomainadminModel.py b/data/Dockerfiles/controller/mailcow-adm/tests/test_DomainadminModel.py deleted file mode 100644 index 0ca50f620..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/tests/test_DomainadminModel.py +++ /dev/null @@ -1,89 +0,0 @@ -import pytest -import json -import sys -import os - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) -from models.DomainModel import DomainModel -from models.DomainadminModel import DomainadminModel - - -def test_model(): - # Generate random domainadmin - random_username = f"dadmin_test{os.urandom(4).hex()}" - random_password = f"{os.urandom(4).hex()}" - - # Create an instance of DomainadminModel - model = DomainadminModel( - username=random_username, - password=random_password, - domains="mailcow.local", - ) - - # Test the parser_command attribute - assert model.parser_command == "domainadmin", "Parser command should be 'domainadmin'" - - # add Domain for testing - domain_model = DomainModel(domain="mailcow.local") - domain_model.add() - - # 1. Domainadmin add tests, should success - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[0]['type'] == "success", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[0]['msg'][0] == "domain_admin_added", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'domain_admin_added'\n{json.dumps(r_add, indent=2)}" - - # 2. Domainadmin add tests, should fail because the domainadmin already exists - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[0]['msg'][0] == "object_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'object_exists'\n{json.dumps(r_add, indent=2)}" - - # 3. Domainadmin get tests - r_get = model.get() - assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}" - assert "selected_domains" in r_get, f"'selected_domains' key missing in response: {json.dumps(r_get, indent=2)}" - assert "username" in r_get, f"'username' key missing in response: {json.dumps(r_get, indent=2)}" - assert set(model.domains.replace(" ", "").split(",")) == set(r_get['selected_domains']), f"Wrong 'selected_domains' received: {r_get['selected_domains']}, expected: {model.domains}\n{json.dumps(r_get, indent=2)}" - assert r_get['username'] == model.username, f"Wrong 'username' received: {r_get['username']}, expected: {model.username}\n{json.dumps(r_get, indent=2)}" - - # 4. Domainadmin edit tests - model.active = 0 - r_edit = model.edit() - assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}" - assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}" - assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}" - assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['msg'][0] == "domain_admin_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'domain_admin_modified'\n{json.dumps(r_edit, indent=2)}" - - # 5. Domainadmin delete tests - r_delete = model.delete() - assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}" - assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}" - assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}" - assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['msg'][0] == "domain_admin_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'domain_admin_removed'\n{json.dumps(r_delete, indent=2)}" - - # delete testing Domain - domain_model.delete() - -if __name__ == "__main__": - print("Running DomainadminModel tests...") - test_model() - print("All tests passed!") diff --git a/data/Dockerfiles/controller/mailcow-adm/tests/test_MailboxModel.py b/data/Dockerfiles/controller/mailcow-adm/tests/test_MailboxModel.py deleted file mode 100644 index c78b0c8be..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/tests/test_MailboxModel.py +++ /dev/null @@ -1,89 +0,0 @@ -import pytest -import json -import sys -import os - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) -from models.DomainModel import DomainModel -from models.MailboxModel import MailboxModel - - -def test_model(): - # Generate random mailbox - random_username = f"mbox_test{os.urandom(4).hex()}@mailcow.local" - random_password = f"{os.urandom(4).hex()}" - - # Create an instance of MailboxModel - model = MailboxModel( - username=random_username, - password=random_password - ) - - # Test the parser_command attribute - assert model.parser_command == "mailbox", "Parser command should be 'mailbox'" - - # add Domain for testing - domain_model = DomainModel(domain="mailcow.local") - domain_model.add() - - # 1. Mailbox add tests, should success - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0 and len(r_add) <= 2, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[1], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[1]['type'] == "success", f"Wrong 'type' received: {r_add[1]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[1], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[1]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[1]['msg']) > 0 and len(r_add[1]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[1]['msg'][0] == "mailbox_added", f"Wrong 'msg' received: {r_add[1]['msg'][0]}, expected: 'mailbox_added'\n{json.dumps(r_add, indent=2)}" - - # 2. Mailbox add tests, should fail because the mailbox already exists - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[0]['msg'][0] == "object_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'object_exists'\n{json.dumps(r_add, indent=2)}" - - # 3. Mailbox get tests - r_get = model.get() - assert isinstance(r_get, dict), f"Expected a dict but received: {json.dumps(r_get, indent=2)}" - assert "domain" in r_get, f"'domain' key missing in response: {json.dumps(r_get, indent=2)}" - assert "local_part" in r_get, f"'local_part' key missing in response: {json.dumps(r_get, indent=2)}" - assert r_get['domain'] == model.domain, f"Wrong 'domain' received: {r_get['domain']}, expected: {model.domain}\n{json.dumps(r_get, indent=2)}" - assert r_get['local_part'] == model.local_part, f"Wrong 'local_part' received: {r_get['local_part']}, expected: {model.local_part}\n{json.dumps(r_get, indent=2)}" - - # 4. Mailbox edit tests - model.active = 0 - r_edit = model.edit() - assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}" - assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}" - assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}" - assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['msg'][0] == "mailbox_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'mailbox_modified'\n{json.dumps(r_edit, indent=2)}" - - # 5. Mailbox delete tests - r_delete = model.delete() - assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}" - assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}" - assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}" - assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['msg'][0] == "mailbox_removed", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'mailbox_removed'\n{json.dumps(r_delete, indent=2)}" - - # delete testing Domain - domain_model.delete() - - -if __name__ == "__main__": - print("Running MailboxModel tests...") - test_model() - print("All tests passed!") diff --git a/data/Dockerfiles/controller/mailcow-adm/tests/test_StatusModel.py b/data/Dockerfiles/controller/mailcow-adm/tests/test_StatusModel.py deleted file mode 100644 index df3f1b2bd..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/tests/test_StatusModel.py +++ /dev/null @@ -1,39 +0,0 @@ -import pytest -import json -import sys -import os - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) -from models.StatusModel import StatusModel - - -def test_model(): - # Create an instance of StatusModel - model = StatusModel() - - # Test the parser_command attribute - assert model.parser_command == "status", "Parser command should be 'status'" - - # 1. Status version tests - r_version = model.version() - assert isinstance(r_version, dict), f"Expected a dict but received: {json.dumps(r_version, indent=2)}" - assert "version" in r_version, f"'version' key missing in response: {json.dumps(r_version, indent=2)}" - - # 2. Status vmail tests - r_vmail = model.vmail() - assert isinstance(r_vmail, dict), f"Expected a dict but received: {json.dumps(r_vmail, indent=2)}" - assert "type" in r_vmail, f"'type' key missing in response: {json.dumps(r_vmail, indent=2)}" - assert "disk" in r_vmail, f"'disk' key missing in response: {json.dumps(r_vmail, indent=2)}" - assert "used" in r_vmail, f"'used' key missing in response: {json.dumps(r_vmail, indent=2)}" - assert "total" in r_vmail, f"'total' key missing in response: {json.dumps(r_vmail, indent=2)}" - assert "used_percent" in r_vmail, f"'used_percent' key missing in response: {json.dumps(r_vmail, indent=2)}" - - # 3. Status containers tests - r_containers = model.containers() - assert isinstance(r_containers, dict), f"Expected a dict but received: {json.dumps(r_containers, indent=2)}" - - -if __name__ == "__main__": - print("Running StatusModel tests...") - test_model() - print("All tests passed!") diff --git a/data/Dockerfiles/controller/mailcow-adm/tests/test_SyncjobModel.py b/data/Dockerfiles/controller/mailcow-adm/tests/test_SyncjobModel.py deleted file mode 100644 index e5d57081a..000000000 --- a/data/Dockerfiles/controller/mailcow-adm/tests/test_SyncjobModel.py +++ /dev/null @@ -1,106 +0,0 @@ -import pytest -import json -import sys -import os - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))) -from models.DomainModel import DomainModel -from models.MailboxModel import MailboxModel -from models.SyncjobModel import SyncjobModel - - -def test_model(): - # Generate random Mailbox - random_username = f"mbox_test@mailcow.local" - random_password = f"{os.urandom(4).hex()}" - - # Create an instance of SyncjobModel - model = SyncjobModel( - username=random_username, - host1="mailcow.local", - port1=993, - user1="testuser@mailcow.local", - password1="testpassword", - enc1="SSL", - ) - - # Test the parser_command attribute - assert model.parser_command == "syncjob", "Parser command should be 'syncjob'" - - # add Domain and Mailbox for testing - domain_model = DomainModel(domain="mailcow.local") - domain_model.add() - mbox_model = MailboxModel(username=random_username, password=random_password) - mbox_model.add() - - # 1. Syncjob add tests, should success - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0 and len(r_add) <= 2, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[0]['type'] == "success", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 3, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[0]['msg'][0] == "mailbox_modified", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'mailbox_modified'\n{json.dumps(r_add, indent=2)}" - - # Assign created syncjob ID for further tests - model.id = r_add[0]['msg'][2] - - # 2. Syncjob add tests, should fail because the syncjob already exists - r_add = model.add() - assert isinstance(r_add, list), f"Expected a array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add) > 0, f"Wrong array received: {json.dumps(r_add, indent=2)}" - assert "type" in r_add[0], f"'type' key missing in response: {json.dumps(r_add, indent=2)}" - assert r_add[0]['type'] == "danger", f"Wrong 'type' received: {r_add[0]['type']}\n{json.dumps(r_add, indent=2)}" - assert "msg" in r_add[0], f"'msg' key missing in response: {json.dumps(r_add, indent=2)}" - assert isinstance(r_add[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_add, indent=2)}" - assert len(r_add[0]['msg']) > 0 and len(r_add[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_add, indent=2)}" - assert r_add[0]['msg'][0] == "object_exists", f"Wrong 'msg' received: {r_add[0]['msg'][0]}, expected: 'object_exists'\n{json.dumps(r_add, indent=2)}" - - # 3. Syncjob get tests - r_get = model.get() - assert isinstance(r_get, list), f"Expected a list but received: {json.dumps(r_get, indent=2)}" - assert "user2" in r_get[0], f"'user2' key missing in response: {json.dumps(r_get, indent=2)}" - assert "host1" in r_get[0], f"'host1' key missing in response: {json.dumps(r_get, indent=2)}" - assert "port1" in r_get[0], f"'port1' key missing in response: {json.dumps(r_get, indent=2)}" - assert "user1" in r_get[0], f"'user1' key missing in response: {json.dumps(r_get, indent=2)}" - assert "enc1" in r_get[0], f"'enc1' key missing in response: {json.dumps(r_get, indent=2)}" - assert r_get[0]['user2'] == model.username, f"Wrong 'user2' received: {r_get[0]['user2']}, expected: {model.username}\n{json.dumps(r_get, indent=2)}" - assert r_get[0]['host1'] == model.host1, f"Wrong 'host1' received: {r_get[0]['host1']}, expected: {model.host1}\n{json.dumps(r_get, indent=2)}" - assert r_get[0]['port1'] == model.port1, f"Wrong 'port1' received: {r_get[0]['port1']}, expected: {model.port1}\n{json.dumps(r_get, indent=2)}" - assert r_get[0]['user1'] == model.user1, f"Wrong 'user1' received: {r_get[0]['user1']}, expected: {model.user1}\n{json.dumps(r_get, indent=2)}" - assert r_get[0]['enc1'] == model.enc1, f"Wrong 'enc1' received: {r_get[0]['enc1']}, expected: {model.enc1}\n{json.dumps(r_get, indent=2)}" - - # 4. Syncjob edit tests - model.active = 1 - r_edit = model.edit() - assert isinstance(r_edit, list), f"Expected a array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit) > 0, f"Wrong array received: {json.dumps(r_edit, indent=2)}" - assert "type" in r_edit[0], f"'type' key missing in response: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['type'] == "success", f"Wrong 'type' received: {r_edit[0]['type']}\n{json.dumps(r_edit, indent=2)}" - assert "msg" in r_edit[0], f"'msg' key missing in response: {json.dumps(r_edit, indent=2)}" - assert isinstance(r_edit[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_edit, indent=2)}" - assert len(r_edit[0]['msg']) > 0 and len(r_edit[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_edit, indent=2)}" - assert r_edit[0]['msg'][0] == "mailbox_modified", f"Wrong 'msg' received: {r_edit[0]['msg'][0]}, expected: 'mailbox_modified'\n{json.dumps(r_edit, indent=2)}" - - # 5. Syncjob delete tests - r_delete = model.delete() - assert isinstance(r_delete, list), f"Expected a array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete) > 0, f"Wrong array received: {json.dumps(r_delete, indent=2)}" - assert "type" in r_delete[0], f"'type' key missing in response: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['type'] == "success", f"Wrong 'type' received: {r_delete[0]['type']}\n{json.dumps(r_delete, indent=2)}" - assert "msg" in r_delete[0], f"'msg' key missing in response: {json.dumps(r_delete, indent=2)}" - assert isinstance(r_delete[0]['msg'], list), f"Expected a 'msg' array but received: {json.dumps(r_delete, indent=2)}" - assert len(r_delete[0]['msg']) > 0 and len(r_delete[0]['msg']) <= 2, f"Wrong 'msg' array received: {json.dumps(r_delete, indent=2)}" - assert r_delete[0]['msg'][0] == "deleted_syncjob", f"Wrong 'msg' received: {r_delete[0]['msg'][0]}, expected: 'deleted_syncjob'\n{json.dumps(r_delete, indent=2)}" - - # delete testing Domain and Mailbox - mbox_model.delete() - domain_model.delete() - - -if __name__ == "__main__": - print("Running SyncjobModel tests...") - test_model() - print("All tests passed!") diff --git a/data/Dockerfiles/controller/stop-supervisor.sh b/data/Dockerfiles/controller/stop-supervisor.sh deleted file mode 100755 index 5394490ce..000000000 --- a/data/Dockerfiles/controller/stop-supervisor.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -printf "READY\n"; - -while read line; do - echo "Processing Event: $line" >&2; - kill -3 $(cat "/var/run/supervisord.pid") -done < /dev/stdin diff --git a/data/Dockerfiles/controller/supervisord.conf b/data/Dockerfiles/controller/supervisord.conf deleted file mode 100644 index 71190e0fc..000000000 --- a/data/Dockerfiles/controller/supervisord.conf +++ /dev/null @@ -1,17 +0,0 @@ -[supervisord] -nodaemon=true -user=root -pidfile=/var/run/supervisord.pid - -[program:api] -command=python /app/api/main.py -autostart=true -autorestart=true -stdout_logfile=/dev/stdout -stderr_logfile=/dev/stderr -stdout_logfile_maxbytes=0 -stderr_logfile_maxbytes=0 - -[eventlistener:processes] -command=/usr/local/sbin/stop-supervisor.sh -events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL diff --git a/data/Dockerfiles/controller/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile similarity index 54% rename from data/Dockerfiles/controller/Dockerfile rename to data/Dockerfiles/dockerapi/Dockerfile index 56e382cb3..c27f6154b 100644 --- a/data/Dockerfiles/controller/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21 +FROM alpine:3.23 LABEL maintainer = "The Infrastructure Company GmbH " @@ -6,29 +6,22 @@ ARG PIP_BREAK_SYSTEM_PACKAGES=1 WORKDIR /app RUN apk add --update --no-cache python3 \ - bash \ py3-pip \ openssl \ tzdata \ py3-psutil \ py3-redis \ py3-async-timeout \ - supervisor \ - curl \ && pip3 install --upgrade pip \ fastapi \ uvicorn \ aiodocker \ docker - -COPY mailcow-adm/ /app/mailcow-adm/ -RUN pip3 install -r /app/mailcow-adm/requirements.txt - -COPY api/ /app/api/ +RUN mkdir /app/modules COPY docker-entrypoint.sh /app/ -COPY supervisord.conf /etc/supervisor/supervisord.conf -COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh +COPY main.py /app/main.py +COPY modules/ /app/modules/ ENTRYPOINT ["/bin/sh", "/app/docker-entrypoint.sh"] -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] +CMD ["python", "main.py"] \ No newline at end of file diff --git a/data/Dockerfiles/dockerapi/docker-entrypoint.sh b/data/Dockerfiles/dockerapi/docker-entrypoint.sh new file mode 100755 index 000000000..64f4b8295 --- /dev/null +++ b/data/Dockerfiles/dockerapi/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +`openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ + -keyout /app/dockerapi_key.pem \ + -out /app/dockerapi_cert.pem \ + -subj /CN=dockerapi/O=mailcow \ + -addext subjectAltName=DNS:dockerapi` + +exec "$@" diff --git a/data/Dockerfiles/controller/api/main.py b/data/Dockerfiles/dockerapi/main.py similarity index 98% rename from data/Dockerfiles/controller/api/main.py rename to data/Dockerfiles/dockerapi/main.py index 69fb764c9..bf197bd61 100644 --- a/data/Dockerfiles/controller/api/main.py +++ b/data/Dockerfiles/dockerapi/main.py @@ -110,12 +110,12 @@ async def get_container(container_id : str): return Response(content=json.dumps(res, indent=4), media_type="application/json") @app.get("/containers/json") -async def get_containers(): +async def get_containers(all: bool = False): global dockerapi containers = {} try: - for container in (await dockerapi.async_docker_client.containers.list()): + for container in (await dockerapi.async_docker_client.containers.list(all=all)): container_info = await container.show() containers.update({container_info['Id']: container_info}) return Response(content=json.dumps(containers, indent=4), media_type="application/json") @@ -254,8 +254,8 @@ if __name__ == '__main__': app, host="0.0.0.0", port=443, - ssl_certfile="/app/controller_cert.pem", - ssl_keyfile="/app/controller_key.pem", + ssl_certfile="/app/dockerapi_cert.pem", + ssl_keyfile="/app/dockerapi_key.pem", log_level="info", loop="none" ) diff --git a/data/Dockerfiles/controller/api/modules/DockerApi.py b/data/Dockerfiles/dockerapi/modules/DockerApi.py similarity index 100% rename from data/Dockerfiles/controller/api/modules/DockerApi.py rename to data/Dockerfiles/dockerapi/modules/DockerApi.py diff --git a/data/Dockerfiles/controller/api/modules/__init__.py b/data/Dockerfiles/dockerapi/modules/__init__.py similarity index 100% rename from data/Dockerfiles/controller/api/modules/__init__.py rename to data/Dockerfiles/dockerapi/modules/__init__.py diff --git a/data/Dockerfiles/dovecot/quarantine_notify.py b/data/Dockerfiles/dovecot/quarantine_notify.py index a681c1fda..5757cdf53 100755 --- a/data/Dockerfiles/dovecot/quarantine_notify.py +++ b/data/Dockerfiles/dovecot/quarantine_notify.py @@ -47,7 +47,7 @@ try: if max_score == "": max_score = 9999.0 - def query_mysql(query, headers = True, update = False): + def query_mysql(query, params = None, headers = True, update = False): while True: try: cnx = MySQLdb.connect(user=os.environ.get('DBUSER'), password=os.environ.get('DBPASS'), database=os.environ.get('DBNAME'), charset="utf8mb4", collation="utf8mb4_general_ci") @@ -57,7 +57,10 @@ try: else: break cur = cnx.cursor() - cur.execute(query) + if params: + cur.execute(query, params) + else: + cur.execute(query) if not update: result = [] columns = tuple( [d[0] for d in cur.description] ) @@ -76,7 +79,7 @@ try: def notify_rcpt(rcpt, msg_count, quarantine_acl, category): if category == "add_header": category = "add header" - meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = "%s" AND score < %f AND (action = "%s" OR "all" = "%s")' % (rcpt, max_score, category, category)) + meta_query = query_mysql('SELECT `qhash`, id, subject, score, sender, created, action FROM quarantine WHERE notified = 0 AND rcpt = %s AND score < %s AND (action = %s OR "all" = %s)', (rcpt, max_score, category, category)) print("%s: %d of %d messages qualify for notification" % (rcpt, len(meta_query), msg_count)) if len(meta_query) == 0: return @@ -130,7 +133,7 @@ try: server.sendmail(msg['From'], [str(redirect)] + [str(bcc)], text) server.quit() for res in meta_query: - query_mysql('UPDATE quarantine SET notified = 1 WHERE id = "%d"' % (res['id']), update = True) + query_mysql('UPDATE quarantine SET notified = 1 WHERE id = %s', (res['id'],), update = True) r.hset('Q_LAST_NOTIFIED', record['rcpt'], time_now) break except Exception as ex: @@ -138,7 +141,7 @@ try: print('%s' % (ex)) time.sleep(3) - records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %f AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt' % (max_score)) + records = query_mysql('SELECT IFNULL(user_acl.quarantine, 0) AS quarantine_acl, count(id) AS counter, rcpt FROM quarantine LEFT OUTER JOIN user_acl ON user_acl.username = rcpt WHERE notified = 0 AND score < %s AND rcpt in (SELECT username FROM mailbox) GROUP BY rcpt', (max_score,)) for record in records: attrs = '' @@ -156,7 +159,7 @@ try: except Exception as ex: print('Could not determine last notification for %s, assuming never' % (record['rcpt'])) last_notification = 0 - attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = "%s"' % (record['rcpt'])) + attrs_json = query_mysql('SELECT attributes FROM mailbox WHERE username = %s', (record['rcpt'],)) attrs = attrs_json[0]['attributes'] if isinstance(attrs, str): # if attr is str then just load it diff --git a/data/Dockerfiles/dovecot/sa-rules.sh b/data/Dockerfiles/dovecot/sa-rules.sh index 7af0423b3..e948d438c 100755 --- a/data/Dockerfiles/dovecot/sa-rules.sh +++ b/data/Dockerfiles/dovecot/sa-rules.sh @@ -25,11 +25,11 @@ sed -i -e 's/\([^\\]\)\$\([^\/]\)/\1\\$\2/g' /etc/rspamd/custom/sa-rules if [[ "$(cat /etc/rspamd/custom/sa-rules | md5sum | cut -d' ' -f1)" != "${HASH_SA_RULES}" ]]; then CONTAINER_NAME=rspamd-mailcow - CONTAINER_ID=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \ + CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | \ jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | \ jq -rc "select( .name | tostring | contains(\"${CONTAINER_NAME}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") if [[ ! -z ${CONTAINER_ID} ]]; then - curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart + curl --silent --insecure -XPOST --connect-timeout 15 --max-time 120 https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart fi fi diff --git a/data/Dockerfiles/netfilter/Dockerfile b/data/Dockerfiles/netfilter/Dockerfile index 57dbd6e94..70cd49c1a 100644 --- a/data/Dockerfiles/netfilter/Dockerfile +++ b/data/Dockerfiles/netfilter/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21 +FROM alpine:3.23 LABEL maintainer = "The Infrastructure Company GmbH " @@ -40,4 +40,4 @@ COPY ./docker-entrypoint.sh /app/ RUN chmod +x /app/docker-entrypoint.sh -CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"] \ No newline at end of file +CMD ["/bin/sh", "-c", "/app/docker-entrypoint.sh"] diff --git a/data/Dockerfiles/nginx/Dockerfile b/data/Dockerfiles/nginx/Dockerfile index 7d2ce34f3..c4230482a 100644 --- a/data/Dockerfiles/nginx/Dockerfile +++ b/data/Dockerfiles/nginx/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:alpine +FROM nginx:1.30.6-alpine LABEL maintainer "The Infrastructure Company GmbH " ENV PIP_BREAK_SYSTEM_PACKAGES=1 diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index f0971ff5e..af23bc2ec 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.2-fpm-alpine3.21 +FROM php:8.2-fpm-alpine3.24 LABEL maintainer = "The Infrastructure Company GmbH " @@ -7,13 +7,13 @@ ARG APCU_PECL_VERSION=5.1.28 # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?.*)$ ARG IMAGICK_PECL_VERSION=3.8.1 # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?.*)$ -ARG MAILPARSE_PECL_VERSION=3.1.9 +ARG MAILPARSE_PECL_VERSION=3.2.0 # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?.*)$ ARG MEMCACHED_PECL_VERSION=3.4.0 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?.*)$ ARG REDIS_PECL_VERSION=6.3.0 # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?.*)$ -ARG COMPOSER_VERSION=2.8.6 +ARG COMPOSER_VERSION=2.10.2 RUN apk add -U --no-cache autoconf \ aspell-dev \ @@ -47,6 +47,7 @@ RUN apk add -U --no-cache autoconf \ libressl-dev \ librsvg \ libtool \ + libwebp \ libwebp-dev \ libxml2-dev \ libxpm \ @@ -63,13 +64,16 @@ RUN apk add -U --no-cache autoconf \ samba-client \ zlib-dev \ tzdata \ - && pecl install APCu-${APCU_PECL_VERSION} \ - && pecl install imagick-${IMAGICK_PECL_VERSION} \ - && pecl install mailparse-${MAILPARSE_PECL_VERSION} \ - && pecl install memcached-${MEMCACHED_PECL_VERSION} \ - && pecl install redis-${REDIS_PECL_VERSION} \ - && docker-php-ext-enable apcu imagick memcached mailparse redis \ - && pecl clear-cache \ + && docker-php-source extract \ + # PECL extensions from their GitHub tags (pecl.php.net is not reliable enough for builds) + && mkdir -p /usr/src/php/ext/apcu /usr/src/php/ext/imagick /usr/src/php/ext/mailparse /usr/src/php/ext/memcached /usr/src/php/ext/redis \ + && curl -fsSL https://github.com/krakjoe/apcu/archive/refs/tags/v${APCU_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/apcu \ + && curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/${IMAGICK_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/imagick \ + && sed -i "s/@PACKAGE_VERSION@/${IMAGICK_PECL_VERSION}/" /usr/src/php/ext/imagick/php_imagick.h \ + && curl -fsSL https://github.com/php/pecl-mail-mailparse/archive/refs/tags/v${MAILPARSE_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/mailparse \ + && curl -fsSL https://github.com/php-memcached-dev/php-memcached/archive/refs/tags/v${MEMCACHED_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/memcached \ + && curl -fsSL https://github.com/phpredis/phpredis/archive/refs/tags/${REDIS_PECL_VERSION}.tar.gz | tar -xz --strip-components=1 -C /usr/src/php/ext/redis \ + && docker-php-ext-install -j 4 apcu imagick mailparse memcached redis \ && docker-php-ext-configure intl \ && docker-php-ext-configure exif \ && docker-php-ext-configure gd --with-freetype=/usr/include/ \ @@ -83,6 +87,7 @@ RUN apk add -U --no-cache autoconf \ && curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER_VERSION} \ && mv composer.phar /usr/local/bin/composer \ && chmod +x /usr/local/bin/composer \ + && docker-php-source delete \ && apk del --purge autoconf \ aspell-dev \ cyrus-sasl-dev \ diff --git a/data/Dockerfiles/phpfpm/docker-entrypoint.sh b/data/Dockerfiles/phpfpm/docker-entrypoint.sh index b23f7ee8f..d7fa15556 100755 --- a/data/Dockerfiles/phpfpm/docker-entrypoint.sh +++ b/data/Dockerfiles/phpfpm/docker-entrypoint.sh @@ -32,7 +32,7 @@ session.save_path = "tcp://'${REDIS_HOST}':'${REDIS_PORT}'?auth='${REDISPASS}'" # Check mysql_upgrade (master and slave) CONTAINER_ID= until [[ ! -z "${CONTAINER_ID}" ]] && [[ "${CONTAINER_ID}" =~ ^[[:alnum:]]*$ ]]; do - CONTAINER_ID=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) + CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"mysql-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) echo "Could not get mysql-mailcow container id... trying again" sleep 2 done @@ -44,7 +44,7 @@ until [[ ${SQL_UPGRADE_STATUS} == 'success' ]]; do echo "Tried to upgrade MySQL and failed, giving up after ${SQL_LOOP_C} retries and starting container (oops, not good)" break fi - SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json') + SQL_FULL_UPGRADE_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_upgrade"}' --silent -H 'Content-type: application/json') SQL_UPGRADE_STATUS=$(echo ${SQL_FULL_UPGRADE_RETURN} | jq -r .type) SQL_LOOP_C=$((SQL_LOOP_C+1)) echo "SQL upgrade iteration #${SQL_LOOP_C}" @@ -69,12 +69,12 @@ done # doing post-installation stuff, if SQL was upgraded (master and slave) if [ ${SQL_CHANGED} -eq 1 ]; then - POSTFIX=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) + POSTFIX=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" 2> /dev/null | jq -rc "select( .name | tostring | contains(\"postfix-mailcow\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id" 2> /dev/null) if [[ -z "${POSTFIX}" ]] || ! [[ "${POSTFIX}" =~ ^[[:alnum:]]*$ ]]; then echo "Could not determine Postfix container ID, skipping Postfix restart." else echo "Restarting Postfix" - curl -X POST --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg' + curl -X POST --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${POSTFIX}/restart | jq -r '.msg' echo "Sleeping 5 seconds..." sleep 5 fi @@ -83,7 +83,7 @@ fi # Check mysql tz import (master and slave) TZ_CHECK=$(mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "SELECT CONVERT_TZ('2019-11-02 23:33:00','Europe/Berlin','UTC') AS time;" -BN 2> /dev/null) if [[ -z ${TZ_CHECK} ]] || [[ "${TZ_CHECK}" == "NULL" ]]; then - SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json') + SQL_FULL_TZINFO_IMPORT_RETURN=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/exec -d '{"cmd":"system", "task":"mysql_tzinfo_to_sql"}' --silent -H 'Content-type: application/json') echo "MySQL mysql_tzinfo_to_sql - debug output:" echo ${SQL_FULL_TZINFO_IMPORT_RETURN} fi diff --git a/data/Dockerfiles/postfix-tlspol/Dockerfile b/data/Dockerfiles/postfix-tlspol/Dockerfile index 68f6ecced..51e8f775d 100644 --- a/data/Dockerfiles/postfix-tlspol/Dockerfile +++ b/data/Dockerfiles/postfix-tlspol/Dockerfile @@ -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 " ARG DEBIAN_FRONTEND=noninteractive diff --git a/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf b/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf index 3862a3547..022dfc3da 100644 --- a/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf +++ b/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf @@ -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 { diff --git a/data/Dockerfiles/postfix-tlspol/syslog-ng.conf b/data/Dockerfiles/postfix-tlspol/syslog-ng.conf index 7126c1250..70c92b5c0 100644 --- a/data/Dockerfiles/postfix-tlspol/syslog-ng.conf +++ b/data/Dockerfiles/postfix-tlspol/syslog-ng.conf @@ -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 { diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 994612ec4..cf0647692 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bookworm-slim +FROM debian:trixie-slim LABEL maintainer="The Infrastructure Company GmbH " diff --git a/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf b/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf index 8e15932a2..bbc9420fe 100644 --- a/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf +++ b/data/Dockerfiles/postfix/syslog-ng-redis_slave.conf @@ -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 { diff --git a/data/Dockerfiles/postfix/syslog-ng.conf b/data/Dockerfiles/postfix/syslog-ng.conf index fc7d1aa0f..8f09d04a4 100644 --- a/data/Dockerfiles/postfix/syslog-ng.conf +++ b/data/Dockerfiles/postfix/syslog-ng.conf @@ -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 { diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 55bcc9572..534ed943e 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -2,7 +2,7 @@ FROM debian:trixie-slim LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_3.14.2-82~90302bc +ARG RSPAMD_VER=rspamd_4.2.1-1~b7a16ae ARG CODENAME=trixie ENV LC_ALL=C diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index ed7e07ed6..997831303 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -1,47 +1,167 @@ -FROM debian:bookworm-slim +# SOGo built from source to enable security patch application +# Repository: https://github.com/Alinto/sogo +# Version: SOGo-5.12.12 +# +# Applied security patches: +# - +# +# To add new patches, modify SOGO_SECURITY_PATCHES ARG below with space-separated commit hashes + +FROM debian:bookworm LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG DEBIAN_VERSION=bookworm -ARG SOGO_DEBIAN_REPOSITORY=https://packagingv2.sogo.nu/sogo-nightly-debian/ +ARG SOGO_VERSION=SOGo-5.12.12 +ARG SOPE_VERSION=SOPE-5.12.12 +# Security patches to apply (space-separated commit hashes) +ARG SOGO_SECURITY_PATCHES="" # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?.*)$ ARG GOSU_VERSION=1.19 ENV LC_ALL=C -# Prerequisites -RUN echo "Building from repository $SOGO_DEBIAN_REPOSITORY" \ - && apt-get update && apt-get install -y --no-install-recommends \ - apt-transport-https \ - ca-certificates \ - gettext \ - gnupg \ - mariadb-client \ - rsync \ - supervisor \ - syslog-ng \ - syslog-ng-core \ - syslog-ng-mod-redis \ - dirmngr \ - netcat-traditional \ - psmisc \ - wget \ - patch \ +# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size) +RUN apt-get update && apt-get install -y --no-install-recommends \ + # Build dependencies + git \ + build-essential \ + gobjc \ + pkg-config \ + gnustep-make \ + gnustep-base-runtime \ + libgnustep-base-dev \ + libxml2-dev \ + libldap2-dev \ + libssl-dev \ + zlib1g-dev \ + libpq-dev \ + libmariadb-dev-compat \ + libmemcached-dev \ + libsodium-dev \ + libcurl4-openssl-dev \ + libzip-dev \ + libytnef0-dev \ + libwbxml2-dev \ + curl \ + ca-certificates \ + # Runtime dependencies + apt-transport-https \ + gettext \ + gnupg \ + mariadb-client \ + rsync \ + supervisor \ + syslog-ng \ + syslog-ng-core \ + syslog-ng-mod-redis \ + dirmngr \ + netcat-traditional \ + psmisc \ + wget \ + patch \ + libobjc4 \ + libxml2 \ + libldap-2.5-0 \ + libssl3 \ + zlib1g \ + libmariadb3 \ + libmemcached11 \ + libsodium23 \ + libcurl4 \ + libzip4 \ + libytnef0 \ + libwbxml2-1 \ + # Download gosu && dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$dpkgArch" \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true \ - && mkdir /usr/share/doc/sogo \ - && touch /usr/share/doc/sogo/empty.sh \ - && wget -O- https://keys.openpgp.org/vks/v1/by-fingerprint/74FFC6D72B925A34B5D356BDF8A27B36A6E2EAE9 | gpg --dearmor | apt-key add - \ - && echo "deb [trusted=yes] ${SOGO_DEBIAN_REPOSITORY} ${DEBIAN_VERSION} main" > /etc/apt/sources.list.d/sogo.list \ - && apt-get update && apt-get install -y --no-install-recommends \ - sogo \ - sogo-activesync \ - && apt-get autoclean \ + # Build SOPE + && git clone --depth 1 --branch ${SOPE_VERSION} https://github.com/Alinto/sope.git /tmp/sope \ + && cd /tmp/sope \ + && rm -rf .git \ + && . /usr/share/GNUstep/Makefiles/GNUstep.sh \ + && ./configure --prefix=/usr --disable-debug --disable-strip \ + && make -j$(nproc) \ + && make install \ + && cd / \ + && rm -rf /tmp/sope \ + # Build SOGo with security patches + && git clone --depth 1 --branch ${SOGO_VERSION} https://github.com/Alinto/sogo.git /tmp/sogo \ + && cd /tmp/sogo \ + && git config user.email "builder@mailcow.local" \ + && git config user.name "SOGo Builder" \ + && for patch in ${SOGO_SECURITY_PATCHES}; do \ + echo "Applying security patch: ${patch}"; \ + git fetch origin ${patch} && git cherry-pick ${patch}; \ + done \ + && rm -rf .git \ + && . /usr/share/GNUstep/Makefiles/GNUstep.sh \ + && ./configure --disable-debug --disable-strip \ + && make -j$(nproc) \ + && make install \ + && cd /tmp/sogo/ActiveSync \ + && . /usr/share/GNUstep/Makefiles/GNUstep.sh \ + && make -j$(nproc) install \ + && cd / \ + && rm -rf /tmp/sogo \ + # Strip binaries + && strip --strip-unneeded /usr/local/sbin/sogod 2>/dev/null || true \ + && strip --strip-unneeded /usr/local/sbin/sogo-tool 2>/dev/null || true \ + && strip --strip-unneeded /usr/local/sbin/sogo-ealarms-notify 2>/dev/null || true \ + && strip --strip-unneeded /usr/local/sbin/sogo-slapd-sockd 2>/dev/null || true \ + # Remove build dependencies and clean up + && apt-get purge -y --auto-remove \ + git \ + build-essential \ + gobjc \ + gnustep-make \ + libgnustep-base-dev \ + libxml2-dev \ + libldap2-dev \ + libssl-dev \ + zlib1g-dev \ + libpq-dev \ + libmariadb-dev-compat \ + libmemcached-dev \ + libsodium-dev \ + libcurl4-openssl-dev \ + libzip-dev \ + libytnef0-dev \ + curl \ + && apt-get autoremove -y \ + && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ + && rm -rf /usr/share/doc/* \ + && rm -rf /usr/share/man/* \ + && rm -rf /var/cache/debconf/* \ + && rm -rf /tmp/* \ + && rm -rf /root/.cache \ + && find /usr/local/lib -name '*.a' -delete \ + && find /usr/lib -name '*.a' -delete \ + && mkdir -p /usr/share/doc/sogo \ + && touch /usr/share/doc/sogo/empty.sh \ && touch /etc/default/locale +# Configure library paths +RUN echo "/usr/lib64" > /etc/ld.so.conf.d/sogo.conf \ + && echo "/usr/local/lib/sogo" >> /etc/ld.so.conf.d/sogo.conf \ + && echo "/usr/local/lib/GNUstep/Frameworks/SOGo.framework/Versions/5/sogo" >> /etc/ld.so.conf.d/sogo.conf \ + && ldconfig + +# Create sogo user and group +RUN groupadd -r -g 999 sogo \ + && useradd -r -u 999 -g sogo -d /var/lib/sogo -s /bin/bash -c "SOGo Daemon" sogo \ + && mkdir -p /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo \ + && chown -R sogo:sogo /var/lib/sogo /var/run/sogo /var/log/sogo /var/spool/sogo + +# Create symlinks for SOGo binaries +RUN ln -s /usr/local/sbin/sogod /usr/sbin/sogod \ + && ln -s /usr/local/sbin/sogo-tool /usr/sbin/sogo-tool \ + && ln -s /usr/local/sbin/sogo-ealarms-notify /usr/sbin/sogo-ealarms-notify \ + && ln -s /usr/local/sbin/sogo-slapd-sockd /usr/sbin/sogo-slapd-sockd + +# Copy configuration files and scripts COPY ./bootstrap-sogo.sh /bootstrap-sogo.sh COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf @@ -56,4 +176,4 @@ RUN chmod +x /bootstrap-sogo.sh \ ENTRYPOINT ["/docker-entrypoint.sh"] -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] \ No newline at end of file +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/data/Dockerfiles/sogo/acl.diff b/data/Dockerfiles/sogo/acl.diff index 513700388..e6b5d0520 100644 --- a/data/Dockerfiles/sogo/acl.diff +++ b/data/Dockerfiles/sogo/acl.diff @@ -1,5 +1,5 @@ ---- /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200 -+++ /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200 +--- /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:57.987504204 +0200 ++++ /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox 2018-08-17 18:29:35.918291298 +0200 @@ -46,7 +46,7 @@ diff --git a/data/Dockerfiles/sogo/bootstrap-sogo.sh b/data/Dockerfiles/sogo/bootstrap-sogo.sh index 96d8a6919..1b2cd2ace 100755 --- a/data/Dockerfiles/sogo/bootstrap-sogo.sh +++ b/data/Dockerfiles/sogo/bootstrap-sogo.sh @@ -130,18 +130,22 @@ chmod 600 /var/lib/sogo/GNUstep/Defaults/sogod.plist # Patch ACLs #if [[ ${ACL_ANYONE} == 'allow' ]]; then # #enable any or authenticated targets for ACL -# if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then -# patch -R /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; +# if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then +# patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; # fi #else # #disable any or authenticated targets for ACL -# if patch -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then -# patch /usr/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; +# if patch -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff > /dev/null; then +# patch /usr/local/lib/GNUstep/SOGo/Templates/UIxAclEditor.wox < /acl.diff; # fi #fi -if patch -R -sfN --dry-run /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then - patch -R /usr/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff; +# Apply custom UI patch (reverse patch to ADD buttons) +if patch -R -sfN --dry-run /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff > /dev/null; then + echo "Applying navMailcowBtns patch (reverse to add buttons)..." + patch -R /usr/local/lib/GNUstep/SOGo/Templates/UIxTopnavToolbar.wox < /navMailcowBtns.diff; +else + echo "navMailcowBtns patch already applied or cannot be applied" fi # Rename custom logo, if any @@ -149,7 +153,7 @@ fi # Rsync web content echo "Syncing web content with named volume" -rsync -a /usr/lib/GNUstep/SOGo/. /sogo_web/ +rsync -a /usr/local/lib/GNUstep/SOGo/. /sogo_web/ # Chown backup path chown -R sogo:sogo /sogo_backup diff --git a/data/Dockerfiles/unbound/Dockerfile b/data/Dockerfiles/unbound/Dockerfile index 4903750ed..321158e07 100644 --- a/data/Dockerfiles/unbound/Dockerfile +++ b/data/Dockerfiles/unbound/Dockerfile @@ -1,12 +1,16 @@ -FROM alpine:3.21 +FROM alpine:3.23 LABEL maintainer = "The Infrastructure Company GmbH " +# install unbound from alpine:edge to get security patches +# 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 \ curl \ bind-tools \ coreutils \ - unbound \ bash \ openssl \ drill \ diff --git a/data/Dockerfiles/watchdog/Dockerfile b/data/Dockerfiles/watchdog/Dockerfile index 6d8541d79..54854ea1a 100644 --- a/data/Dockerfiles/watchdog/Dockerfile +++ b/data/Dockerfiles/watchdog/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.21 +FROM alpine:3.23 LABEL maintainer = "The Infrastructure Company GmbH " @@ -37,5 +37,6 @@ RUN apk add --update \ COPY watchdog.sh /watchdog.sh COPY check_mysql_slavestatus.sh /usr/lib/nagios/plugins/check_mysql_slavestatus.sh COPY check_dns.sh /usr/lib/mailcow/check_dns.sh +COPY client.cnf /etc/my.cnf.d/client.cnf CMD ["/watchdog.sh"] diff --git a/data/Dockerfiles/watchdog/check_dns.sh b/data/Dockerfiles/watchdog/check_dns.sh index ce4cfa3b1..ba430fe95 100755 --- a/data/Dockerfiles/watchdog/check_dns.sh +++ b/data/Dockerfiles/watchdog/check_dns.sh @@ -19,19 +19,19 @@ if [ -z "$HOST" ]; then fi # run dig and measure the time it takes to run -START_TIME=$(date +%s%3N) +START_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time') dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null) dig_rc=$? +END_TIME=$(perl -MTime::HiRes -e 'print Time::HiRes::time') dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -) -END_TIME=$(date +%s%3N) -ELAPSED_TIME=$((END_TIME - START_TIME)) +ELAPSED_TIME=$(perl -e "printf('%.3f', $END_TIME - $START_TIME)") # validate and perform nagios like output and exit codes if [ $dig_rc -ne 0 ] || [ -z "$dig_output" ]; then echo "Domain $HOST was not found by the server" exit 2 elif [ $dig_rc -eq 0 ]; then - echo "DNS OK: $ELAPSED_TIME ms response time. $HOST returns $dig_output_ips" + echo "DNS OK: $ELAPSED_TIME seconds response time. $HOST returns $dig_output_ips" exit 0 else echo "Unknown error" diff --git a/data/Dockerfiles/watchdog/client.cnf b/data/Dockerfiles/watchdog/client.cnf new file mode 100644 index 000000000..f078f3cbe --- /dev/null +++ b/data/Dockerfiles/watchdog/client.cnf @@ -0,0 +1,3 @@ +[client] +ssl = false +ssl-verify-server-cert = false diff --git a/data/Dockerfiles/watchdog/watchdog.sh b/data/Dockerfiles/watchdog/watchdog.sh index b99fa3d7d..020f3f838 100755 --- a/data/Dockerfiles/watchdog/watchdog.sh +++ b/data/Dockerfiles/watchdog/watchdog.sh @@ -38,7 +38,7 @@ if [[ ! -p /tmp/com_pipe ]]; then fi # Wait for containers -while ! mariadb-admin status --ssl=false --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do +while ! mariadb-admin status --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u${DBUSER} -p${DBPASS} --silent; do echo "Waiting for SQL..." sleep 2 done @@ -200,12 +200,12 @@ get_container_ip() { else sleep 0.5 # get long container id for exact match - CONTAINER_ID=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")) + CONTAINER_ID=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring == \"${1}\") | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id")) # returned id can have multiple elements (if scaled), shuffle for random test CONTAINER_ID=($(printf "%s\n" "${CONTAINER_ID[@]}" | shuf)) if [[ ! -z ${CONTAINER_ID} ]]; then for matched_container in "${CONTAINER_ID[@]}"; do - CONTAINER_IPS=($(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) + CONTAINER_IPS=($(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${matched_container}/json | jq -r '.NetworkSettings.Networks[].IPAddress')) for ip_match in "${CONTAINER_IPS[@]}"; do # grep will do nothing if one of these vars is empty [[ -z ${ip_match} ]] && continue @@ -359,8 +359,8 @@ mysql_checks() { while [ ${err_count} -lt ${THRESHOLD} ]; do touch /tmp/mysql-mailcow; echo "$(tail -50 /tmp/mysql-mailcow)" > /tmp/mysql-mailcow err_c_cur=${err_count} - /usr/lib/nagios/plugins/check_mysql -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? )) - /usr/lib/nagios/plugins/check_mysql_query -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_mysql -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_mysql_query -f /etc/my.cnf.d/client.cnf -s /var/run/mysqld/mysqld.sock -u ${DBUSER} -p ${DBPASS} -d ${DBNAME} -q "SELECT COUNT(*) FROM information_schema.tables" 2>> /tmp/mysql-mailcow 1>&2; err_count=$(( ${err_count} + $? )) [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) progress "MySQL/MariaDB" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} @@ -384,7 +384,7 @@ mysql_repl_checks() { while [ ${err_count} -lt ${THRESHOLD} ]; do touch /tmp/mysql_repl_checks; echo "$(tail -50 /tmp/mysql_repl_checks)" > /tmp/mysql_repl_checks err_c_cur=${err_count} - /usr/lib/nagios/plugins/check_mysql_slavestatus.sh -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/nagios/plugins/check_mysql_slavestatus.sh -o /etc/my.cnf.d/client.cnf -S /var/run/mysqld/mysqld.sock -u root -p ${DBROOT} 2>> /tmp/mysql_repl_checks 1>&2; err_count=$(( ${err_count} + $? )) [ ${err_c_cur} -eq ${err_count} ] && [ ! $((${err_count} - 1)) -lt 0 ] && err_count=$((${err_count} - 1)) diff_c=1 [ ${err_c_cur} -ne ${err_count} ] && diff_c=$(( ${err_c_cur} - ${err_count} )) progress "MySQL/MariaDB replication" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} @@ -1075,15 +1075,15 @@ while true; do done ) & -# Monitor controller +# Monitor dockerapi ( while true; do - while nc -z controller 443; do + while nc -z dockerapi 443; do sleep 3 done - log_msg "Cannot find controller-mailcow, waiting to recover..." + log_msg "Cannot find dockerapi-mailcow, waiting to recover..." kill -STOP ${BACKGROUND_TASKS[*]} - until nc -z controller 443; do + until nc -z dockerapi 443; do sleep 3 done kill -CONT ${BACKGROUND_TASKS[*]} @@ -1143,12 +1143,12 @@ while true; do elif [[ ${com_pipe_answer} =~ .+-mailcow ]]; then kill -STOP ${BACKGROUND_TASKS[*]} sleep 10 - CONTAINER_ID=$(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") + CONTAINER_ID=$(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/json | jq -r ".[] | {name: .Config.Labels[\"com.docker.compose.service\"], project: .Config.Labels[\"com.docker.compose.project\"], id: .Id}" | jq -rc "select( .name | tostring | contains(\"${com_pipe_answer}\")) | select( .project | tostring | contains(\"${COMPOSE_PROJECT_NAME,,}\")) | .id") if [[ ! -z ${CONTAINER_ID} ]]; then if [[ "${com_pipe_answer}" == "php-fpm-mailcow" ]]; then - HAS_INITDB=$(curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true) + HAS_INITDB=$(curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/top | jq '.msg.Processes[] | contains(["php -c /usr/local/etc/php -f /web/inc/init_db.inc.php"])' | grep true) fi - S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d))) + S_RUNNING=$(($(date +%s) - $(curl --silent --insecure https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/json | jq .State.StartedAt | xargs -n1 date +%s -d))) if [ ${S_RUNNING} -lt 360 ]; then log_msg "Container is running for less than 360 seconds, skipping action..." elif [[ ! -z ${HAS_INITDB} ]]; then @@ -1156,7 +1156,7 @@ while true; do sleep 60 else log_msg "Sending restart command to ${CONTAINER_ID}..." - curl --silent --insecure -XPOST https://controller.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart + curl --silent --insecure -XPOST https://dockerapi.${COMPOSE_PROJECT_NAME}_mailcow-network/containers/${CONTAINER_ID}/restart notify_error "${com_pipe_answer}" log_msg "Wait for restarted container to settle and continue watching..." sleep 35 diff --git a/data/Dockerfiles/controller/mailcow-adm/models/__init__.py b/data/conf/acme/.gitkeep similarity index 100% rename from data/Dockerfiles/controller/mailcow-adm/models/__init__.py rename to data/conf/acme/.gitkeep diff --git a/data/conf/dovecot/auth/passwd-verify.lua b/data/conf/dovecot/auth/passwd-verify.lua index ea847932d..a22488ea8 100644 --- a/data/conf/dovecot/auth/passwd-verify.lua +++ b/data/conf/dovecot/auth/passwd-verify.lua @@ -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 diff --git a/data/conf/dovecot/global_sieve_before b/data/conf/dovecot/global_sieve_before index 3e79ca1d3..e71be5100 100644 --- a/data/conf/dovecot/global_sieve_before +++ b/data/conf/dovecot/global_sieve_before @@ -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; -} diff --git a/data/conf/nginx/templates/nginx.conf.j2 b/data/conf/nginx/templates/nginx.conf.j2 index 08a85a144..861c7e560 100644 --- a/data/conf/nginx/templates/nginx.conf.j2 +++ b/data/conf/nginx/templates/nginx.conf.j2 @@ -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(' ') }}; diff --git a/data/conf/nginx/templates/sites-default.conf.j2 b/data/conf/nginx/templates/sites-default.conf.j2 index 992bb9ce2..842c50a54 100644 --- a/data/conf/nginx/templates/sites-default.conf.j2 +++ b/data/conf/nginx/templates/sites-default.conf.j2 @@ -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; } @@ -185,7 +188,6 @@ location ^~ /Microsoft-Server-ActiveSync { auth_request_set $user $upstream_http_x_user; auth_request_set $auth $upstream_http_x_auth; auth_request_set $auth_type $upstream_http_x_auth_type; - auth_request_set $real_ip $remote_addr; proxy_set_header x-webobjects-remote-user "$user"; proxy_set_header Authorization "$auth"; proxy_set_header x-webobjects-auth-type "$auth_type"; @@ -211,7 +213,6 @@ location ^~ /SOGo { auth_request_set $user $upstream_http_x_user; auth_request_set $auth $upstream_http_x_auth; auth_request_set $auth_type $upstream_http_x_auth_type; - auth_request_set $real_ip $remote_addr; proxy_set_header x-webobjects-remote-user "$user"; proxy_set_header Authorization "$auth"; proxy_set_header x-webobjects-auth-type "$auth_type"; @@ -234,7 +235,6 @@ location ^~ /SOGo { auth_request_set $user $upstream_http_x_user; auth_request_set $auth $upstream_http_x_auth; auth_request_set $auth_type $upstream_http_x_auth_type; - auth_request_set $real_ip $remote_addr; proxy_set_header x-webobjects-remote-user "$user"; proxy_set_header Authorization "$auth"; proxy_set_header x-webobjects-auth-type "$auth_type"; @@ -264,19 +264,19 @@ location ~* /sogo$ { } location /SOGo.woa/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; + alias /usr/local/lib/GNUstep/SOGo/WebServerResources/; } location /.woa/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; + alias /usr/local/lib/GNUstep/SOGo/WebServerResources/; } location /SOGo/WebServerResources/ { - alias /usr/lib/GNUstep/SOGo/WebServerResources/; + alias /usr/local/lib/GNUstep/SOGo/WebServerResources/; } location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$) { - alias /usr/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; + alias /usr/local/lib/GNUstep/SOGo/$1.SOGo/Resources/$2; } {% endif %} diff --git a/data/conf/phpfpm/php-conf.d/other.ini b/data/conf/phpfpm/php-conf.d/other.ini index 02f59a9c0..e60e9f815 100644 --- a/data/conf/phpfpm/php-conf.d/other.ini +++ b/data/conf/phpfpm/php-conf.d/other.ini @@ -1,3 +1,5 @@ max_execution_time = 3600 max_input_time = 3600 memory_limit = 512M +; not used by mailcow, disabled as hardening (PHP security release 2026-10-21) +session.upload_progress.enabled = Off diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index f091cb3f9..4329f5487 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -37,7 +37,7 @@ postscreen_access_list = permit_mynetworks, cidr:/opt/postfix/conf/postscreen_access.cidr, tcp:127.0.0.1:10027 postscreen_bare_newline_enable = no -postscreen_blacklist_action = drop +postscreen_denylist_action = drop postscreen_cache_cleanup_interval = 24h postscreen_cache_map = proxy:btree:$data_directory/postscreen_cache postscreen_dnsbl_action = enforce @@ -107,8 +107,6 @@ smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch, reject_unknown_sender_domain smtpd_soft_error_limit = 3 smtpd_tls_auth_only = yes -smtpd_tls_dh1024_param_file = /etc/ssl/mail/dhparams.pem -smtpd_tls_eecdh_grade = auto smtpd_tls_exclude_ciphers = ECDHE-RSA-RC4-SHA, RC4, aNULL, DES-CBC3-SHA, ECDHE-RSA-DES-CBC3-SHA, EDH-RSA-DES-CBC3-SHA smtpd_tls_loglevel = 1 @@ -152,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 "*" diff --git a/data/conf/postfix/master.cf b/data/conf/postfix/master.cf index d5114df28..fb49f2df2 100644 --- a/data/conf/postfix/master.cf +++ b/data/conf/postfix/master.cf @@ -29,7 +29,6 @@ smtps inet n - n - - smtpd # TLS protocol can be modified by setting submission_smtpd_tls_mandatory_protocols in extra.cf submission inet n - n - - smtpd -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject - -o smtpd_enforce_tls=yes -o smtpd_tls_security_level=encrypt -o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols -o tls_preempt_cipherlist=yes @@ -38,7 +37,6 @@ submission inet n - n - - smtpd 10587 inet n - n - - smtpd -o smtpd_upstream_proxy_protocol=haproxy -o smtpd_client_restrictions=permit_mynetworks,permit_sasl_authenticated,reject - -o smtpd_enforce_tls=yes -o smtpd_tls_security_level=encrypt -o smtpd_tls_mandatory_protocols=$submission_smtpd_tls_mandatory_protocols -o tls_preempt_cipherlist=yes diff --git a/data/conf/postfix/postscreen_access.cidr b/data/conf/postfix/postscreen_access.cidr index 9ff9f6265..b479644db 100644 --- a/data/conf/postfix/postscreen_access.cidr +++ b/data/conf/postfix/postscreen_access.cidr @@ -1,7 +1,8 @@ -# Whitelist generated by Postwhite v3.4 on Thu Jan 1 00:24:01 UTC 2026 +# Whitelist generated by Postwhite v3.4 on Thu Oct 1 00:35:20 UTC 2026 # https://github.com/stevejenkins/postwhite/ -# 2105 total rules +# 2246 total rules 2a00:1450:4000::/36 permit +2a00:1450:4864::/56 permit 2a01:111:f400::/48 permit 2a01:111:f403:2800::/53 permit 2a01:111:f403:8000::/51 permit @@ -28,11 +29,15 @@ 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 +2c0f:fb50:4864::/56 permit +2.207.151.32/27 permit 2.207.151.53 permit 2.207.217.30 permit +2.207.223.160/27 permit 3.64.237.68 permit 3.65.3.180 permit 3.70.123.177 permit @@ -52,10 +57,15 @@ 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.107.213.38 permit -13.107.246.38 permit 13.108.16.0/20 permit 13.110.208.0/21 permit 13.110.209.0/24 permit @@ -65,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 @@ -114,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 @@ -136,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 @@ -168,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 @@ -179,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 @@ -191,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 @@ -251,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 @@ -270,8 +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 @@ -328,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 @@ -358,7 +358,9 @@ 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 64.207.219.8 permit 64.207.219.9 permit @@ -393,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 @@ -443,7 +446,11 @@ 66.218.75.252/31 permit 66.218.75.254 permit 66.220.144.128/25 permit +66.220.144.178 permit +66.220.144.179 permit 66.220.155.0/24 permit +66.220.155.178 permit +66.220.155.179 permit 66.220.157.0/25 permit 66.231.80.0/20 permit 66.240.227.0/24 permit @@ -508,6 +515,8 @@ 69.162.98.0/24 permit 69.169.224.0/20 permit 69.171.232.0/24 permit +69.171.232.180 permit +69.171.232.181 permit 69.171.244.0/23 permit 70.42.149.35 permit 72.3.185.0/24 permit @@ -629,6 +638,8 @@ 77.238.189.148/30 permit 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 @@ -647,6 +658,9 @@ 82.165.159.45 permit 82.165.159.130 permit 82.165.159.131 permit +84.8.68.0/25 permit +84.8.192.128/25 permit +84.8.224.128/25 permit 85.9.206.169 permit 85.9.210.45 permit 85.158.136.0/21 permit @@ -697,7 +711,9 @@ 87.248.117.205 permit 87.253.232.0/21 permit 89.22.108.0/24 permit -91.198.2.0/24 permit +91.198.2.177 permit +91.198.2.217 permit +91.198.2.222 permit 91.211.240.0/22 permit 94.236.119.0/26 permit 95.131.104.0/21 permit @@ -708,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 @@ -1194,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 @@ -1354,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 @@ -1390,6 +1412,20 @@ 129.146.88.28 permit 129.146.147.105 permit 129.146.236.58 permit +129.148.135.0/25 permit +129.148.148.0/25 permit +129.148.164.0/25 permit +129.148.180.0/25 permit +129.148.215.0/25 permit +129.149.6.0/25 permit +129.149.22.0/25 permit +129.149.38.0/25 permit +129.149.52.0/25 permit +129.149.68.0/25 permit +129.149.84.0/25 permit +129.149.100.0/25 permit +129.149.118.0/25 permit +129.149.126.0/25 permit 129.151.67.221 permit 129.153.62.216 permit 129.153.104.71 permit @@ -1402,24 +1438,41 @@ 129.159.22.159 permit 129.159.87.137 permit 129.213.195.191 permit +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 131.253.30.0/24 permit 131.253.121.0/26 permit 132.145.13.209 permit 132.226.26.225 permit 132.226.49.32 permit 132.226.56.24 permit -134.128.64.0/19 permit -134.128.96.0/19 permit +134.98.248.128/25 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 @@ -1428,6 +1481,10 @@ 136.147.182.0/24 permit 136.147.224.0/20 permit 136.179.50.206 permit +136.248.224.128/25 permit +136.248.232.128/25 permit +138.1.108.0/25 permit +138.1.170.0/24 permit 139.60.152.0/22 permit 139.138.35.44 permit 139.138.46.121 permit @@ -1435,6 +1492,8 @@ 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 141.148.55.217 permit @@ -1465,14 +1524,14 @@ 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 146.148.116.76 permit 147.154.32.0/25 permit +147.154.63.0/24 permit +147.154.126.0/24 permit +147.154.191.0/24 permit 147.243.1.47 permit 147.243.1.48 permit 147.243.1.153 permit @@ -1480,16 +1539,23 @@ 147.243.128.26 permit 148.105.0.0/16 permit 148.105.8.0/21 permit +148.116.32.128/25 permit 149.72.0.0/16 permit 149.72.234.184 permit 149.72.248.236 permit 149.97.173.180 permit +149.118.160.128/25 permit 150.136.21.199 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 +155.248.140.0/25 permit +155.248.148.0/25 permit 155.248.208.51 permit 155.248.220.138 permit 155.248.234.149 permit @@ -1498,10 +1564,16 @@ 157.58.30.128/25 permit 157.58.196.96/29 permit 157.58.249.3 permit +157.137.32.128/25 permit +157.137.96.128/25 permit 157.151.208.65 permit 157.255.1.64/29 permit 158.101.211.207 permit 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 @@ -1523,16 +1595,21 @@ 159.135.224.0/20 permit 159.135.228.10 permit 159.183.0.0/16 permit +159.183.14.233 permit 159.183.68.71 permit 159.183.79.38 permit +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 161.71.64.0/20 permit 162.88.4.0/23 permit 162.88.8.0/24 permit +162.88.24.0/23 permit 162.88.24.0/24 permit 162.88.25.0/24 permit 162.88.36.0/24 permit @@ -1550,7 +1627,13 @@ 164.152.23.32 permit 164.152.25.241 permit 164.177.132.168/30 permit -166.78.68.0/22 permit +165.1.100.0/25 permit +165.173.128.0/24 permit +165.173.180.0/24 permit +165.173.180.250/31 permit +165.173.182.0/24 permit +165.173.182.250/31 permit +165.173.189.205 permit 166.78.68.221 permit 166.78.69.169 permit 166.78.69.170 permit @@ -1568,9 +1651,12 @@ 167.89.75.126 permit 167.89.75.136 permit 167.89.75.164 permit -167.89.101.2 permit -167.89.101.192/28 permit 167.220.67.232/29 permit +168.107.248.128/25 permit +168.110.160.128/25 permit +168.110.248.128/25 permit +168.129.184.128/25 permit +168.129.248.128/25 permit 168.138.5.36 permit 168.138.73.51 permit 168.138.77.31 permit @@ -1579,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 @@ -1593,6 +1692,7 @@ 173.203.81.39 permit 173.224.161.128/25 permit 173.224.165.0/26 permit +173.224.166.48/28 permit 174.36.84.8/29 permit 174.36.84.16/29 permit 174.36.84.32/29 permit @@ -1607,13 +1707,14 @@ 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 183.240.219.64/29 permit 185.4.120.0/22 permit -185.11.253.128/27 permit -185.11.255.0/24 permit +185.11.255.144 permit 185.12.80.0/22 permit 185.28.196.0/22 permit 185.58.84.93 permit @@ -1627,8 +1728,16 @@ 185.138.56.128/25 permit 185.189.236.0/22 permit 185.211.120.0/22 permit -185.233.188.0/23 permit -185.233.190.0/23 permit +185.233.188.68 permit +185.233.188.75 permit +185.233.188.84 permit +185.233.188.160 permit +185.233.188.176 permit +185.233.188.247 permit +185.233.189.44 permit +185.233.189.98 permit +185.233.189.122 permit +185.233.189.228 permit 185.250.236.0/22 permit 185.250.239.148 permit 185.250.239.168 permit @@ -1681,30 +1790,42 @@ 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 +192.22.32.128/25 permit +192.22.96.128/25 permit +192.22.160.128/25 permit +192.22.224.128/25 permit 192.28.128.0/18 permit +192.29.24.0/25 permit +192.29.44.0/25 permit +192.29.72.0/25 permit +192.29.88.0/25 permit 192.29.103.128/25 permit +192.29.134.0/25 permit +192.29.151.128/25 permit +192.29.172.0/25 permit +192.29.178.0/25 permit +192.29.200.0/25 permit +192.29.216.0/25 permit +192.29.232.0/25 permit +192.29.248.0/25 permit 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 -192.254.112.60 permit -192.254.112.98/31 permit -192.254.113.10 permit -192.254.113.101 permit -192.254.114.176 permit 193.109.254.0/23 permit 193.122.128.100 permit 193.123.56.63 permit -193.142.157.0/24 permit +193.142.157.15 permit +193.142.157.125 permit +193.142.157.158 permit 193.142.157.191 permit 193.142.157.198 permit 194.19.134.0/25 permit @@ -1738,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 @@ -1764,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 @@ -1816,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 @@ -1856,6 +1990,9 @@ 207.211.30.128/25 permit 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 @@ -1863,6 +2000,8 @@ 208.43.21.28/30 permit 208.43.21.64/29 permit 208.43.21.72/30 permit +208.56.9.224 permit +208.56.13.196 permit 208.64.132.0/22 permit 208.71.40.63 permit 208.71.40.64/31 permit @@ -1889,6 +2028,7 @@ 208.71.42.214 permit 208.72.249.240/29 permit 208.75.120.0/22 permit +208.76.62.0/23 permit 208.76.62.0/24 permit 208.76.63.0/24 permit 208.82.237.96/29 permit @@ -1902,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 @@ -1997,8 +2136,6 @@ 212.227.126.225 permit 212.227.126.226 permit 212.227.126.227 permit -213.95.19.64/27 permit -213.95.135.4 permit 213.199.128.139 permit 213.199.128.145 permit 213.199.138.181 permit @@ -2051,6 +2188,7 @@ 216.136.168.80/28 permit 216.139.64.0/19 permit 216.145.221.0/24 permit +216.146.32.0/23 permit 216.146.32.0/24 permit 216.146.33.0/24 permit 216.198.0.0/18 permit @@ -2071,6 +2209,7 @@ 223.165.120.0/23 permit 2001:0868:0100:0600::/64 permit 2001:4860:4000::/36 permit +2001:4860:4864::/56 permit 2001:748:100:40::2:0/112 permit 2001:748:400:1300::3 permit 2001:748:400:1300::4 permit @@ -2088,12 +2227,13 @@ 2001:748:400:3301::3 permit 2001:748:400:3301::4 permit 2404:6800:4000::/36 permit -2603:1010:3:3::5b permit -2603:1020:201:10::10f permit -2603:1030:20e:3::23c permit -2603:1030:b:3::152 permit -2603:1030:c02:8::14 permit +2404:6800:4864::/56 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 2607:f8b0:4000::/36 permit +2607:f8b0:4864::/56 permit 2620:109:c003:104::/64 permit 2620:109:c003:104::215 permit 2620:109:c006:104::/64 permit @@ -2106,5 +2246,6 @@ 2620:119:50c0:207::/64 permit 2620:119:50c0:207::215 permit 2800:3f0:4000::/36 permit +2800:3f0:4864::/56 permit 49.12.4.251 permit # checks.mailcow.email 2a01:4f8:c17:7906::10 permit # checks.mailcow.email diff --git a/data/conf/rspamd/local.d/metadata_exporter.conf b/data/conf/rspamd/local.d/metadata_exporter.conf index daaa79b4e..cc85faacd 100644 --- a/data/conf/rspamd/local.d/metadata_exporter.conf +++ b/data/conf/rspamd/local.d/metadata_exporter.conf @@ -3,8 +3,7 @@ rules { backend = "http"; url = "http://nginx:9081/pipe.php"; selector = "reject_no_global_bl"; - formatter = "default"; - meta_headers = true; + formatter = "multipart"; } RLINFO { backend = "http"; @@ -16,8 +15,7 @@ rules { backend = "http"; url = "http://nginx:9081/pushover.php"; selector = "mailcow_rcpt"; - formatter = "json"; - meta_headers = true; + formatter = "multipart"; } } diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua index 503b41e6d..5d7bcaaf3 100644 --- a/data/conf/rspamd/lua/rspamd.local.lua +++ b/data/conf/rspamd/lua/rspamd.local.lua @@ -392,6 +392,7 @@ rspamd_config:register_symbol({ local rspamd_http = require "rspamd_http" local rcpts = task:get_recipients('smtp') local lua_util = require "lua_util" + local tagged_rcpt = task:get_symbol("TAGGED_RCPT") local function remove_moo_tag() local moo_tag_header = task:get_header('X-Moo-Tag', false) @@ -416,12 +417,9 @@ rspamd_config:register_symbol({ -- Check if recipient has a tag (contains '+') local tag = nil - if rcpt_user:find('%+') then - local base_user, tag_part = rcpt_user:match('^(.-)%+(.+)$') - if base_user and tag_part then - tag = tag_part - rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag) - end + if tagged_rcpt ~= nil then + tag = tagged_rcpt + rspamd_logger.infox("TAG_MOO: found tag in recipient: %s (base: %s, tag: %s)", rcpt_addr, base_user, tag) end if not tag then @@ -500,7 +498,8 @@ rspamd_config:register_symbol({ else rspamd_logger.infox("TAG_MOO: user wants subject modified for tagged mail") local sbj = task:get_header('Subject') or '' - new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag .. '] ' .. sbj)) .. '?=' + local tag_value = tag[1] and tag[1].options and tag[1].options[1] or '' + new_sbj = '=?UTF-8?B?' .. tostring(util.encode_base64('[' .. tag_value .. '] ' .. sbj)) .. '?=' task:set_milter_reply({ remove_headers = { ['Subject'] = 1, @@ -857,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 @@ -878,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 @@ -896,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 @@ -915,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 @@ -945,4 +946,4 @@ rspamd_config:register_symbol({ return true end, priority = 1 -}) \ No newline at end of file +}) diff --git a/data/conf/rspamd/meta_exporter/pipe.php b/data/conf/rspamd/meta_exporter/pipe.php index 003639b08..ab0d1faa0 100644 --- a/data/conf/rspamd/meta_exporter/pipe.php +++ b/data/conf/rspamd/meta_exporter/pipe.php @@ -32,47 +32,42 @@ function parse_email($email) { $a = strrpos($email, '@'); return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); } -if (!function_exists('getallheaders')) { - function getallheaders() { - if (!is_array($_SERVER)) { - return array(); - } - $headers = array(); - foreach ($_SERVER as $name => $value) { - if (substr($name, 0, 5) == 'HTTP_') { - $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; - } - } - return $headers; - } +// rspamd metadata_exporter (multipart formatter): +// - $_POST['metadata'] JSON with the rspamd metadata +// - $_FILES['message'] raw RFC822 message +if (empty($_POST['metadata']) || !isset($_FILES['message']) || $_FILES['message']['error'] !== UPLOAD_ERR_OK) { + error_log("QUARANTINE: missing multipart parts from rspamd" . PHP_EOL); + http_response_code(400); + exit; } -$raw_data_content = file_get_contents('php://input'); +$meta = json_decode($_POST['metadata'], true); +if (!is_array($meta)) { + error_log("QUARANTINE: cannot decode metadata JSON" . PHP_EOL); + http_response_code(400); + exit; +} + +$raw_data_content = file_get_contents($_FILES['message']['tmp_name']); $raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8"); -$headers = getallheaders(); +$raw_size = (int)$_FILES['message']['size']; -$qid = $headers['X-Rspamd-Qid']; -$fuzzy = $headers['X-Rspamd-Fuzzy']; -$subject = iconv_mime_decode($headers['X-Rspamd-Subject']); -$score = $headers['X-Rspamd-Score']; -$rcpts = $headers['X-Rspamd-Rcpt']; -$user = $headers['X-Rspamd-User']; -$ip = $headers['X-Rspamd-Ip']; -$action = $headers['X-Rspamd-Action']; -$sender = $headers['X-Rspamd-From']; -$symbols = $headers['X-Rspamd-Symbols']; - -$raw_size = (int)$_SERVER['CONTENT_LENGTH']; +$qid = $meta['qid'] ?? 'unknown'; +$subject = $meta['subject'] ?? ''; +$score = $meta['score'] ?? 0; +$rcpts = $meta['rcpt'] ?? array(); +$user = $meta['user'] ?? 'unknown'; +$ip = $meta['ip'] ?? 'unknown'; +$action = $meta['action'] ?? 'no action'; +$sender = $meta['from'] ?? ''; +$symbols = json_encode($meta['symbols'] ?? array()); +$fuzzy = json_encode(is_array($meta['fuzzy'] ?? null) ? $meta['fuzzy'] : array()); if (empty($sender)) { error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL); $sender = 'empty-env-from@localhost'; } -if ($fuzzy == 'unknown') { - $fuzzy = '[]'; -} - try { $max_size = (int)$redis->Get('Q_MAX_SIZE'); if (($max_size * 1048576) < $raw_size) { @@ -94,7 +89,7 @@ catch (RedisException $e) { $rcpt_final_mailboxes = array(); // Loop through all rcpts -foreach (json_decode($rcpts, true) as $rcpt) { +foreach ($rcpts as $rcpt) { // Remove tag $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); @@ -158,12 +153,12 @@ foreach (json_decode($rcpts, true) 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; @@ -172,21 +167,21 @@ foreach (json_decode($rcpts, true) 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); } } @@ -208,11 +203,11 @@ foreach (json_decode($rcpts, true) 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; } diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index efe5fdd52..e937c6956 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -32,50 +32,46 @@ function parse_email($email) { $a = strrpos($email, '@'); return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1)); } -if (!function_exists('getallheaders')) { - function getallheaders() { - if (!is_array($_SERVER)) { - return array(); - } - $headers = array(); - foreach ($_SERVER as $name => $value) { - if (substr($name, 0, 5) == 'HTTP_') { - $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; - } - } - return $headers; - } +// rspamd metadata_exporter (multipart formatter): metadata JSON arrives as $_POST['metadata']. +if (empty($_POST['metadata'])) { + error_log("NOTIFY: missing metadata part from rspamd" . PHP_EOL); + http_response_code(400); + exit; } -$headers = getallheaders(); -$json_body = json_decode(file_get_contents('php://input')); +$meta = json_decode($_POST['metadata'], true); +if (!is_array($meta)) { + error_log("NOTIFY: cannot decode metadata JSON" . PHP_EOL); + http_response_code(400); + exit; +} -$qid = $headers['X-Rspamd-Qid']; -$rcpts = $headers['X-Rspamd-Rcpt']; -$sender = $headers['X-Rspamd-From']; -$ip = $headers['X-Rspamd-Ip']; -$subject = iconv_mime_decode($headers['X-Rspamd-Subject']); -$messageid= $json_body->message_id; +$qid = $meta['qid'] ?? 'unknown'; +$rcpts = $meta['rcpt'] ?? array(); +$sender = $meta['from'] ?? ''; +$ip = $meta['ip'] ?? 'unknown'; +$subject = $meta['subject'] ?? ''; +$messageid= $meta['message_id'] ?? ''; $priority = 0; -$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true); +$symbols_array = $meta['symbols'] ?? array(); if (is_array($symbols_array)) { foreach ($symbols_array as $symbol) { - if ($symbol['name'] == 'HAS_X_PRIO_ONE') { + if (($symbol['name'] ?? null) == 'HAS_X_PRIO_ONE') { $priority = 1; break; } } } -$sender_address = $json_body->header_from[0]; +$sender_address = $meta['header_from'][0] ?? ''; $sender_name = '-'; if (preg_match('/(?.*?)<(?
.*?)>/i', $sender_address, $matches)) { $sender_address = $matches['address']; $sender_name = trim($matches['name'], '"\' '); } -$to_address = $json_body->header_to[0]; +$to_address = $meta['header_to'][0] ?? ''; $to_name = '-'; if (preg_match('/(?.*?)<(?
.*?)>/i', $to_address, $matches)) { $to_address = $matches['address']; @@ -85,7 +81,7 @@ if (preg_match('/(?.*?)<(?
.*?)>/i', $to_address, $matches)) { $rcpt_final_mailboxes = array(); // Loop through all rcpts -foreach (json_decode($rcpts, true) as $rcpt) { +foreach ($rcpts as $rcpt) { // Remove tag $rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt); @@ -143,12 +139,12 @@ foreach (json_decode($rcpts, true) 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; @@ -157,21 +153,21 @@ foreach (json_decode($rcpts, true) 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); } } @@ -193,11 +189,11 @@ foreach (json_decode($rcpts, true) 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; } diff --git a/data/web/admin/dashboard.php b/data/web/admin/dashboard.php index 443c2ea2a..2867770bc 100644 --- a/data/web/admin/dashboard.php +++ b/data/web/admin/dashboard.php @@ -2,18 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") { - header('Location: /admin'); - exit(); -} +protect_route(['admin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; diff --git a/data/web/admin/index.php b/data/web/admin/index.php index 9ae4a0380..9e0bf1bd7 100644 --- a/data/web/admin/index.php +++ b/data/web/admin/index.php @@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin/dashboard'); - exit(); + // Only redirect to dashboard if NO pending actions + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + header('Location: /admin/dashboard'); + exit(); + } } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { header('Location: /domainadmin/mailbox'); diff --git a/data/web/admin/mailbox.php b/data/web/admin/mailbox.php index d0073bbd6..7b07d6fa9 100644 --- a/data/web/admin/mailbox.php +++ b/data/web/admin/mailbox.php @@ -2,18 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") { - header('Location: /admin'); - exit(); -} +protect_route(['admin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; diff --git a/data/web/admin/queue.php b/data/web/admin/queue.php index 85ec59401..66682c7ae 100644 --- a/data/web/admin/queue.php +++ b/data/web/admin/queue.php @@ -2,19 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") { - header('Location: /admin'); - exit(); -} - +protect_route(['admin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $js_minifier->add('/web/js/site/queue.js'); diff --git a/data/web/admin/system.php b/data/web/admin/system.php index 9fd44e0d8..b5e00808f 100644 --- a/data/web/admin/system.php +++ b/data/web/admin/system.php @@ -2,18 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.admin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "admin") { - header('Location: /admin'); - exit(); -} +protect_route(['admin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; @@ -126,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, diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index c4e255951..c86561903 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -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: @@ -1072,6 +1084,7 @@ paths: password2: "*" quota: "3072" force_pw_update: "1" + force_tfa: "1" tls_enforce_in: "1" tls_enforce_out: "1" tags: ["tag1", "tag2"] @@ -1118,6 +1131,7 @@ paths: password2: atedismonsin quota: "3072" force_pw_update: "1" + force_tfa: "1" tls_enforce_in: "1" tls_enforce_out: "1" tags: ["tag1", "tag2"] @@ -1151,6 +1165,9 @@ paths: force_pw_update: description: forces the user to update its password on first login type: boolean + force_tfa: + description: force 2FA enrollment at login + type: boolean tls_enforce_in: description: force inbound email tls encryption type: boolean @@ -2454,6 +2471,90 @@ paths: type: object type: object summary: Delete mails in Quarantine + /api/v1/edit/qitem: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + release: + value: + - log: + - quarantine + - edit + - id: + - "33" + action: release + msg: + - item_released + - "33" + type: success + learnham: + value: + - log: + - quarantine + - edit + - id: + - "34" + action: learnham + msg: + - item_learned + - "34" + type: success + schema: + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string + type: object + description: OK + headers: {} + tags: + - Quarantine + description: >- + Using this endpoint you can perform actions on quarantine items. It is possible to release + emails from quarantine into to the inbox, or learn them as ham to improve Rspamd filtering. + You must provide the quarantine item IDs. You can get the IDs using the GET method. + operationId: Edit mails in Quarantine + requestBody: + content: + application/json: + schema: + example: + items: + - "33" + - "34" + attr: + action: release + properties: + items: + description: contains list of quarantine item IDs to release or learn as ham + type: object + attr: + description: attributes for the action + type: object + properties: + action: + type: string + enum: + - release + - learnham + description: "release - return email to inbox; learnham - learn as ham to improve filtering" + type: object + summary: Edit mails in Quarantine /api/v1/delete/recipient_map: post: responses: @@ -3330,6 +3431,7 @@ paths: - mailbox - active: "1" force_pw_update: "0" + force_tfa: "0" name: Full name password: "*" password2: "*" @@ -3380,6 +3482,7 @@ paths: attr: active: "1" force_pw_update: "0" + force_tfa: "0" name: Full name authsource: mailcow password: "" @@ -3403,6 +3506,9 @@ paths: force_pw_update: description: force user to change password on next login type: boolean + force_tfa: + description: force 2FA enrollment at login + type: boolean name: description: Full name of the mailbox user type: string @@ -4797,6 +4903,7 @@ paths: - active: "1" attributes: force_pw_update: "0" + force_tfa: "0" mailbox_format: "maildir:" quarantine_notification: never sogo_access: "1" @@ -4813,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"] @@ -5352,9 +5460,9 @@ paths: started_at: "2019-12-22T21:00:01.622856172Z" state: running type: info - controller-mailcow: - container: controller-mailcow - image: "mailcow/controller:1.36" + dockerapi-mailcow: + container: dockerapi-mailcow + image: "mailcow/dockerapi:1.36" started_at: "2019-12-22T20:59:59.984797808Z" state: running type: info @@ -5721,6 +5829,7 @@ paths: - active: "1" attributes: force_pw_update: "0" + force_tfa: "0" mailbox_format: "maildir:" quarantine_notification: never sogo_access: "1" @@ -5738,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"] @@ -5760,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: { } @@ -5777,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: diff --git a/data/web/autodiscover.php b/data/web/autodiscover.php index d3cda4004..3e31dc3dd 100644 --- a/data/web/autodiscover.php +++ b/data/web/autodiscover.php @@ -60,101 +60,31 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt); $iam_provider = identity_provider('init'); $iam_settings = identity_provider('get'); -$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER'])); -$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW'])); +// Passwordless autodiscover - no authentication required +// Email will be extracted from the request body +$login_user = null; +$login_role = null; -if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) { - $json = json_encode( - array( - "time" => time(), - "ua" => $_SERVER['HTTP_USER_AGENT'], - "user" => "none", - "ip" => $_SERVER['REMOTE_ADDR'], - "service" => "Error: must be authenticated" - ) - ); - $redis->lPush('AUTODISCOVER_LOG', $json); - header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"'); - header('HTTP/1.0 401 Unauthorized'); - exit(0); -} - -$login_role = check_login($login_user, $login_pass, array('service' => 'EAS')); - -if ($login_role === "user") { - header("Content-Type: application/xml"); - echo '' . PHP_EOL; +header("Content-Type: application/xml"); +echo '' . PHP_EOL; ?> time(), - "ua" => $_SERVER['HTTP_USER_AGENT'], - "user" => $_SERVER['PHP_AUTH_USER'], - "ip" => $_SERVER['REMOTE_ADDR'], - "service" => "Error: invalid or missing request data" - ) - ); - $redis->lPush('AUTODISCOVER_LOG', $json); - $redis->lTrim('AUTODISCOVER_LOG', 0, 100); - } - catch (RedisException $e) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'msg' => 'Redis: '.$e - ); - return false; - } - list($usec, $sec) = explode(' ', microtime()); -?> - - - 600 - Invalid Request - - - - -Request->EMailAddress; - } catch (Exception $e) { - $email = $_SERVER['PHP_AUTH_USER']; - } - - $username = trim($email); - try { - $stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username"); - $stmt->execute(array(':username' => $username)); - $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - die("Failed to determine name from SQL"); - } - if (!empty($MailboxData['name'])) { - $displayname = $MailboxData['name']; - } - else { - $displayname = $email; - } +if(!$data) { try { $json = json_encode( array( "time" => time(), "ua" => $_SERVER['HTTP_USER_AGENT'], - "user" => $_SERVER['PHP_AUTH_USER'], + "user" => "none", "ip" => $_SERVER['REMOTE_ADDR'], - "service" => $autodiscover_config['autodiscoverType'] + "service" => "Error: invalid or missing request data" ) ); $redis->lPush('AUTODISCOVER_LOG', $json); $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + $redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']); + error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']); } catch (RedisException $e) { $_SESSION['return'][] = array( @@ -163,7 +93,143 @@ if ($login_role === "user") { ); return false; } - if ($autodiscover_config['autodiscoverType'] == 'imap') { + list($usec, $sec) = explode(' ', microtime()); +?> + + + 600 + Invalid Request + + + + +Request->EMailAddress; +} catch (Exception $e) { + // If parsing fails, return error + try { + $json = json_encode( + array( + "time" => time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => "none", + "ip" => $_SERVER['REMOTE_ADDR'], + "service" => "Error: could not parse email from request" + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + $redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']); + error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']); + } + catch (RedisException $e) { + // Silently fail + } + list($usec, $sec) = explode(' ', microtime()); +?> + + + 600 + Invalid Request + + + + +prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox` + INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain` + WHERE `mailbox`.`username` = :username + AND `mailbox`.`active` = '1' + AND `domain`.`active` = '1'"); + $stmt->execute(array(':username' => $username)); + $MailboxData = $stmt->fetch(PDO::FETCH_ASSOC); +} +catch(PDOException $e) { + // Database error - return error response with complete XML + list($usec, $sec) = explode(' ', microtime()); +?> + + + 500 + Database Error + + + + + time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => $email, + "ip" => $_SERVER['REMOTE_ADDR'], + "service" => "Error: mailbox not found or inactive" + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); + $redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']); + error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']); + } + catch (RedisException $e) { + // Silently fail + } + list($usec, $sec) = explode(' ', microtime()); +?> + + + 600 + Invalid Request + + + + + time(), + "ua" => $_SERVER['HTTP_USER_AGENT'], + "user" => $email, + "ip" => $_SERVER['REMOTE_ADDR'], + "service" => $autodiscover_config['autodiscoverType'] + ) + ); + $redis->lPush('AUTODISCOVER_LOG', $json); + $redis->lTrim('AUTODISCOVER_LOG', 0, 100); +} +catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'msg' => 'Redis: '.$e + ); + return false; +} +if ($autodiscover_config['autodiscoverType'] == 'imap') { ?> @@ -238,6 +304,3 @@ if ($login_role === "user") { } ?> - diff --git a/data/web/css/build/003-bootstrap-select.css b/data/web/css/build/003-bootstrap-select.css index e248901c1..f9ce6d901 100644 --- a/data/web/css/build/003-bootstrap-select.css +++ b/data/web/css/build/003-bootstrap-select.css @@ -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 { diff --git a/data/web/css/build/013-datatables.css b/data/web/css/build/013-datatables.css index 2b19ba24e..1bc18119e 100644 --- a/data/web/css/build/013-datatables.css +++ b/data/web/css/build/013-datatables.css @@ -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; diff --git a/data/web/css/build/014-mailcow.css b/data/web/css/build/014-mailcow.css index 3e1ace79d..52a34f8b5 100644 --- a/data/web/css/build/014-mailcow.css +++ b/data/web/css/build/014-mailcow.css @@ -34,11 +34,44 @@ url('/fonts/noto-sans-v12-latin_greek_cyrillic-700italic.woff') format('woff'); } +/* theme tokens, override in css/themes//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 { diff --git a/data/web/css/themes/lumen-bootstrap.css b/data/web/css/themes/lumen-bootstrap.css index bcf626839..65ef3ca34 100644 --- a/data/web/css/themes/lumen-bootstrap.css +++ b/data/web/css/themes/lumen-bootstrap.css @@ -1,17 +1,16 @@ @charset "UTF-8"; /*! - * Bootswatch v5.1.3 (https://bootswatch.com) - * Copyright 2012-2022 Thomas Park + * Bootswatch v5.3.8 (https://bootswatch.com) + * Theme: lumen + * Copyright 2012-2025 Thomas Park * Licensed under MIT * Based on Bootstrap */ /*! - * Bootstrap v5.1.3 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. + * Bootstrap v5.3.8 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ - /* source-sans-pro-300 - latin */ @font-face { font-family: 'Source Sans Pro'; @@ -90,8 +89,8 @@ url('/fonts/source-sans-pro-v21-latin-700italic.ttf') format('truetype'), /* Safari, Android, iOS */ url('/fonts/source-sans-pro-v21-latin-700italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */ } - -:root { +:root, +[data-bs-theme=light] { --bs-blue: #158cba; --bs-indigo: #6610f2; --bs-purple: #6f42c1; @@ -102,6 +101,7 @@ --bs-green: #28b62c; --bs-teal: #20c997; --bs-cyan: #75caeb; + --bs-black: #000; --bs-white: #fff; --bs-gray: #999; --bs-gray-dark: #333; @@ -130,10 +130,32 @@ --bs-danger-rgb: 255, 65, 54; --bs-light-rgb: 246, 246, 246; --bs-dark-rgb: 85, 85, 85; + --bs-primary-text-emphasis: #08384a; + --bs-secondary-text-emphasis: #606060; + --bs-success-text-emphasis: #104912; + --bs-info-text-emphasis: #2f515e; + --bs-warning-text-emphasis: #66350b; + --bs-danger-text-emphasis: #661a16; + --bs-light-text-emphasis: #555; + --bs-dark-text-emphasis: #555; + --bs-primary-bg-subtle: #d0e8f1; + --bs-secondary-bg-subtle: #fcfcfc; + --bs-success-bg-subtle: #d4f0d5; + --bs-info-bg-subtle: #e3f4fb; + --bs-warning-bg-subtle: #ffe7d1; + --bs-danger-bg-subtle: #ffd9d7; + --bs-light-bg-subtle: #fbfbfb; + --bs-dark-bg-subtle: #ced4da; + --bs-primary-border-subtle: #a1d1e3; + --bs-secondary-border-subtle: #f9f9f9; + --bs-success-border-subtle: #a9e2ab; + --bs-info-border-subtle: #c8eaf7; + --bs-warning-border-subtle: #ffcea4; + --bs-danger-border-subtle: #ffb3af; + --bs-light-border-subtle: #f0f0f0; + --bs-dark-border-subtle: #adb5bd; --bs-white-rgb: 255, 255, 255; --bs-black-rgb: 0, 0, 0; - --bs-body-color-rgb: 34, 34, 34; - --bs-body-bg-rgb: 255, 255, 255; --bs-font-sans-serif: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); @@ -142,7 +164,106 @@ --bs-body-font-weight: 400; --bs-body-line-height: 1.5; --bs-body-color: #222; + --bs-body-color-rgb: 34, 34, 34; --bs-body-bg: #fff; + --bs-body-bg-rgb: 255, 255, 255; + --bs-emphasis-color: #000; + --bs-emphasis-color-rgb: 0, 0, 0; + --bs-secondary-color: rgba(34, 34, 34, 0.75); + --bs-secondary-color-rgb: 34, 34, 34; + --bs-secondary-bg: #f0f0f0; + --bs-secondary-bg-rgb: 240, 240, 240; + --bs-tertiary-color: rgba(34, 34, 34, 0.5); + --bs-tertiary-color-rgb: 34, 34, 34; + --bs-tertiary-bg: #f6f6f6; + --bs-tertiary-bg-rgb: 246, 246, 246; + --bs-heading-color: inherit; + --bs-link-color: #158cba; + --bs-link-color-rgb: 21, 140, 186; + --bs-link-decoration: underline; + --bs-link-hover-color: #117095; + --bs-link-hover-color-rgb: 17, 112, 149; + --bs-code-color: #e83e8c; + --bs-highlight-color: #222; + --bs-highlight-bg: #ffe7d1; + --bs-border-width: 1px; + --bs-border-style: solid; + --bs-border-color: #dee2e6; + --bs-border-color-translucent: rgba(0, 0, 0, 0.175); + --bs-border-radius: 0.375rem; + --bs-border-radius-sm: 0.25rem; + --bs-border-radius-lg: 0.5rem; + --bs-border-radius-xl: 1rem; + --bs-border-radius-xxl: 2rem; + --bs-border-radius-2xl: var(--bs-border-radius-xxl); + --bs-border-radius-pill: 50rem; + --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); + --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); + --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); + --bs-focus-ring-width: 0.25rem; + --bs-focus-ring-opacity: 0.25; + --bs-focus-ring-color: rgba(21, 140, 186, 0.25); + --bs-form-valid-color: #28b62c; + --bs-form-valid-border-color: #28b62c; + --bs-form-invalid-color: #ff4136; + --bs-form-invalid-border-color: #ff4136; +} + +[data-bs-theme=dark] { + color-scheme: dark; + --bs-body-color: #dee2e6; + --bs-body-color-rgb: 222, 226, 230; + --bs-body-bg: #222; + --bs-body-bg-rgb: 34, 34, 34; + --bs-emphasis-color: #fff; + --bs-emphasis-color-rgb: 255, 255, 255; + --bs-secondary-color: rgba(222, 226, 230, 0.75); + --bs-secondary-color-rgb: 222, 226, 230; + --bs-secondary-bg: #333; + --bs-secondary-bg-rgb: 51, 51, 51; + --bs-tertiary-color: rgba(222, 226, 230, 0.5); + --bs-tertiary-color-rgb: 222, 226, 230; + --bs-tertiary-bg: #2b2b2b; + --bs-tertiary-bg-rgb: 43, 43, 43; + --bs-primary-text-emphasis: #73bad6; + --bs-secondary-text-emphasis: #f6f6f6; + --bs-success-text-emphasis: #7ed380; + --bs-info-text-emphasis: #acdff3; + --bs-warning-text-emphasis: #ffb676; + --bs-danger-text-emphasis: #ff8d86; + --bs-light-text-emphasis: #f6f6f6; + --bs-dark-text-emphasis: #dee2e6; + --bs-primary-bg-subtle: #041c25; + --bs-secondary-bg-subtle: #303030; + --bs-success-bg-subtle: #082409; + --bs-info-bg-subtle: #17282f; + --bs-warning-bg-subtle: #331b05; + --bs-danger-bg-subtle: #330d0b; + --bs-light-bg-subtle: #333; + --bs-dark-bg-subtle: #1a1a1a; + --bs-primary-border-subtle: #0d5470; + --bs-secondary-border-subtle: #909090; + --bs-success-border-subtle: #186d1a; + --bs-info-border-subtle: #46798d; + --bs-warning-border-subtle: #995010; + --bs-danger-border-subtle: #992720; + --bs-light-border-subtle: #555; + --bs-dark-border-subtle: #333; + --bs-heading-color: inherit; + --bs-link-color: #73bad6; + --bs-link-hover-color: #8fc8de; + --bs-link-color-rgb: 115, 186, 214; + --bs-link-hover-color-rgb: 143, 200, 222; + --bs-code-color: #f18bba; + --bs-highlight-color: #dee2e6; + --bs-highlight-bg: #66350b; + --bs-border-color: #555; + --bs-border-color-translucent: rgba(255, 255, 255, 0.15); + --bs-form-valid-color: #7ed380; + --bs-form-valid-border-color: #7ed380; + --bs-form-invalid-color: #ff8d86; + --bs-form-invalid-border-color: #ff8d86; } *, @@ -173,20 +294,17 @@ body { hr { margin: 1rem 0; color: inherit; - background-color: currentColor; border: 0; + border-top: var(--bs-border-width) solid; opacity: 0.25; } -hr:not([size]) { - height: 1px; -} - h6, .h6, h5, .h5, h4, .h4, h3, .h3, h2, .h2, h1, .h1 { margin-top: 0; margin-bottom: 0.5rem; font-weight: 500; line-height: 1.2; + color: var(--bs-heading-color); } h1, .h1 { @@ -238,8 +356,7 @@ p { margin-bottom: 1rem; } -abbr[title], -abbr[data-bs-original-title] { +abbr[title] { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; cursor: help; @@ -295,8 +412,9 @@ small, .small { } mark, .mark { - padding: 0.2em; - background-color: #fcf8e3; + padding: 0.1875em; + color: var(--bs-highlight-color); + background-color: var(--bs-highlight-bg); } sub, @@ -316,11 +434,11 @@ sup { } a { - color: #158cba; + color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); text-decoration: underline; } a:hover { - color: #117095; + --bs-link-color-rgb: var(--bs-link-hover-color-rgb); } a:not([href]):not([class]), a:not([href]):not([class]):hover { @@ -334,8 +452,6 @@ kbd, samp { font-family: var(--bs-font-monospace); font-size: 1em; - direction: ltr /* rtl:ignore */; - unicode-bidi: bidi-override; } pre { @@ -353,7 +469,7 @@ pre code { code { font-size: 0.875em; - color: #e83e8c; + color: var(--bs-code-color); word-wrap: break-word; } a > code { @@ -361,16 +477,15 @@ a > code { } kbd { - padding: 0.2rem 0.4rem; + padding: 0.1875rem 0.375rem; font-size: 0.875em; - color: #fff; - background-color: #222; - border-radius: 0.2rem; + color: var(--bs-body-bg); + background-color: var(--bs-body-color); + border-radius: 0.25rem; } kbd kbd { padding: 0; font-size: 1em; - font-weight: 700; } figure { @@ -390,7 +505,7 @@ table { caption { padding-top: 0.5rem; padding-bottom: 0.5rem; - color: #999; + color: var(--bs-secondary-color); text-align: left; } @@ -449,8 +564,8 @@ select:disabled { opacity: 1; } -[list]::-webkit-calendar-picker-indicator { - display: none; +[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { + display: none !important; } button, @@ -487,8 +602,8 @@ legend { width: 100%; padding: 0; margin-bottom: 0.5rem; - font-size: calc(1.275rem + 0.3vw); line-height: inherit; + font-size: calc(1.275rem + 0.3vw); } @media (min-width: 1200px) { legend { @@ -514,8 +629,12 @@ legend + * { } [type=search] { - outline-offset: -2px; -webkit-appearance: textfield; + outline-offset: -2px; +} +[type=search]::-webkit-search-cancel-button { + cursor: pointer; + filter: grayscale(1); } /* rtl:raw: @@ -536,14 +655,11 @@ legend + * { ::-webkit-file-upload-button { font: inherit; + -webkit-appearance: button; } ::file-selector-button { font: inherit; -} - -::-webkit-file-upload-button { - font: inherit; -webkit-appearance: button; } @@ -574,9 +690,9 @@ progress { } .display-1 { - font-size: calc(1.625rem + 4.5vw); font-weight: 300; line-height: 1.2; + font-size: calc(1.625rem + 4.5vw); } @media (min-width: 1200px) { .display-1 { @@ -585,9 +701,9 @@ progress { } .display-2 { - font-size: calc(1.575rem + 3.9vw); font-weight: 300; line-height: 1.2; + font-size: calc(1.575rem + 3.9vw); } @media (min-width: 1200px) { .display-2 { @@ -596,9 +712,9 @@ progress { } .display-3 { - font-size: calc(1.525rem + 3.3vw); font-weight: 300; line-height: 1.2; + font-size: calc(1.525rem + 3.3vw); } @media (min-width: 1200px) { .display-3 { @@ -607,9 +723,9 @@ progress { } .display-4 { - font-size: calc(1.475rem + 2.7vw); font-weight: 300; line-height: 1.2; + font-size: calc(1.475rem + 2.7vw); } @media (min-width: 1200px) { .display-4 { @@ -618,9 +734,9 @@ progress { } .display-5 { - font-size: calc(1.425rem + 2.1vw); font-weight: 300; line-height: 1.2; + font-size: calc(1.425rem + 2.1vw); } @media (min-width: 1200px) { .display-5 { @@ -629,9 +745,9 @@ progress { } .display-6 { - font-size: calc(1.375rem + 1.5vw); font-weight: 300; line-height: 1.2; + font-size: calc(1.375rem + 1.5vw); } @media (min-width: 1200px) { .display-6 { @@ -676,7 +792,7 @@ progress { color: #999; } .blockquote-footer::before { - content: "— "; + content: "— "; } .img-fluid { @@ -686,9 +802,9 @@ progress { .img-thumbnail { padding: 0.25rem; - background-color: #fff; - border: 1px solid #dee2e6; - border-radius: 0.25rem; + background-color: var(--bs-body-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); max-width: 100%; height: auto; } @@ -704,7 +820,7 @@ progress { .figure-caption { font-size: 0.875em; - color: #999; + color: var(--bs-secondary-color); } .container, @@ -714,9 +830,11 @@ progress { .container-lg, .container-md, .container-sm { + --bs-gutter-x: 1.5rem; + --bs-gutter-y: 0; width: 100%; - padding-right: var(--bs-gutter-x, 0.75rem); - padding-left: var(--bs-gutter-x, 0.75rem); + padding-right: calc(var(--bs-gutter-x) * 0.5); + padding-left: calc(var(--bs-gutter-x) * 0.5); margin-right: auto; margin-left: auto; } @@ -746,6 +864,15 @@ progress { max-width: 1320px; } } +:root { + --bs-breakpoint-xs: 0; + --bs-breakpoint-sm: 576px; + --bs-breakpoint-md: 768px; + --bs-breakpoint-lg: 992px; + --bs-breakpoint-xl: 1200px; + --bs-breakpoint-xxl: 1400px; +} + .row { --bs-gutter-x: 1.5rem; --bs-gutter-y: 0; @@ -765,7 +892,7 @@ progress { } .col { - flex: 1 0 0%; + flex: 1 0 0; } .row-cols-auto > * { @@ -785,7 +912,7 @@ progress { .row-cols-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } .row-cols-4 > * { @@ -800,7 +927,7 @@ progress { .row-cols-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } .col-auto { @@ -974,1089 +1101,875 @@ progress { @media (min-width: 576px) { .col-sm { - flex: 1 0 0%; + flex: 1 0 0; } - .row-cols-sm-auto > * { flex: 0 0 auto; width: auto; } - .row-cols-sm-1 > * { flex: 0 0 auto; width: 100%; } - .row-cols-sm-2 > * { flex: 0 0 auto; width: 50%; } - .row-cols-sm-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } - .row-cols-sm-4 > * { flex: 0 0 auto; width: 25%; } - .row-cols-sm-5 > * { flex: 0 0 auto; width: 20%; } - .row-cols-sm-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } - .col-sm-auto { flex: 0 0 auto; width: auto; } - .col-sm-1 { flex: 0 0 auto; width: 8.33333333%; } - .col-sm-2 { flex: 0 0 auto; width: 16.66666667%; } - .col-sm-3 { flex: 0 0 auto; width: 25%; } - .col-sm-4 { flex: 0 0 auto; width: 33.33333333%; } - .col-sm-5 { flex: 0 0 auto; width: 41.66666667%; } - .col-sm-6 { flex: 0 0 auto; width: 50%; } - .col-sm-7 { flex: 0 0 auto; width: 58.33333333%; } - .col-sm-8 { flex: 0 0 auto; width: 66.66666667%; } - .col-sm-9 { flex: 0 0 auto; width: 75%; } - .col-sm-10 { flex: 0 0 auto; width: 83.33333333%; } - .col-sm-11 { flex: 0 0 auto; width: 91.66666667%; } - .col-sm-12 { flex: 0 0 auto; width: 100%; } - .offset-sm-0 { margin-left: 0; } - .offset-sm-1 { margin-left: 8.33333333%; } - .offset-sm-2 { margin-left: 16.66666667%; } - .offset-sm-3 { margin-left: 25%; } - .offset-sm-4 { margin-left: 33.33333333%; } - .offset-sm-5 { margin-left: 41.66666667%; } - .offset-sm-6 { margin-left: 50%; } - .offset-sm-7 { margin-left: 58.33333333%; } - .offset-sm-8 { margin-left: 66.66666667%; } - .offset-sm-9 { margin-left: 75%; } - .offset-sm-10 { margin-left: 83.33333333%; } - .offset-sm-11 { margin-left: 91.66666667%; } - .g-sm-0, -.gx-sm-0 { + .gx-sm-0 { --bs-gutter-x: 0; } - .g-sm-0, -.gy-sm-0 { + .gy-sm-0 { --bs-gutter-y: 0; } - .g-sm-1, -.gx-sm-1 { + .gx-sm-1 { --bs-gutter-x: 0.25rem; } - .g-sm-1, -.gy-sm-1 { + .gy-sm-1 { --bs-gutter-y: 0.25rem; } - .g-sm-2, -.gx-sm-2 { + .gx-sm-2 { --bs-gutter-x: 0.5rem; } - .g-sm-2, -.gy-sm-2 { + .gy-sm-2 { --bs-gutter-y: 0.5rem; } - .g-sm-3, -.gx-sm-3 { + .gx-sm-3 { --bs-gutter-x: 1rem; } - .g-sm-3, -.gy-sm-3 { + .gy-sm-3 { --bs-gutter-y: 1rem; } - .g-sm-4, -.gx-sm-4 { + .gx-sm-4 { --bs-gutter-x: 1.5rem; } - .g-sm-4, -.gy-sm-4 { + .gy-sm-4 { --bs-gutter-y: 1.5rem; } - .g-sm-5, -.gx-sm-5 { + .gx-sm-5 { --bs-gutter-x: 3rem; } - .g-sm-5, -.gy-sm-5 { + .gy-sm-5 { --bs-gutter-y: 3rem; } } @media (min-width: 768px) { .col-md { - flex: 1 0 0%; + flex: 1 0 0; } - .row-cols-md-auto > * { flex: 0 0 auto; width: auto; } - .row-cols-md-1 > * { flex: 0 0 auto; width: 100%; } - .row-cols-md-2 > * { flex: 0 0 auto; width: 50%; } - .row-cols-md-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } - .row-cols-md-4 > * { flex: 0 0 auto; width: 25%; } - .row-cols-md-5 > * { flex: 0 0 auto; width: 20%; } - .row-cols-md-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } - .col-md-auto { flex: 0 0 auto; width: auto; } - .col-md-1 { flex: 0 0 auto; width: 8.33333333%; } - .col-md-2 { flex: 0 0 auto; width: 16.66666667%; } - .col-md-3 { flex: 0 0 auto; width: 25%; } - .col-md-4 { flex: 0 0 auto; width: 33.33333333%; } - .col-md-5 { flex: 0 0 auto; width: 41.66666667%; } - .col-md-6 { flex: 0 0 auto; width: 50%; } - .col-md-7 { flex: 0 0 auto; width: 58.33333333%; } - .col-md-8 { flex: 0 0 auto; width: 66.66666667%; } - .col-md-9 { flex: 0 0 auto; width: 75%; } - .col-md-10 { flex: 0 0 auto; width: 83.33333333%; } - .col-md-11 { flex: 0 0 auto; width: 91.66666667%; } - .col-md-12 { flex: 0 0 auto; width: 100%; } - .offset-md-0 { margin-left: 0; } - .offset-md-1 { margin-left: 8.33333333%; } - .offset-md-2 { margin-left: 16.66666667%; } - .offset-md-3 { margin-left: 25%; } - .offset-md-4 { margin-left: 33.33333333%; } - .offset-md-5 { margin-left: 41.66666667%; } - .offset-md-6 { margin-left: 50%; } - .offset-md-7 { margin-left: 58.33333333%; } - .offset-md-8 { margin-left: 66.66666667%; } - .offset-md-9 { margin-left: 75%; } - .offset-md-10 { margin-left: 83.33333333%; } - .offset-md-11 { margin-left: 91.66666667%; } - .g-md-0, -.gx-md-0 { + .gx-md-0 { --bs-gutter-x: 0; } - .g-md-0, -.gy-md-0 { + .gy-md-0 { --bs-gutter-y: 0; } - .g-md-1, -.gx-md-1 { + .gx-md-1 { --bs-gutter-x: 0.25rem; } - .g-md-1, -.gy-md-1 { + .gy-md-1 { --bs-gutter-y: 0.25rem; } - .g-md-2, -.gx-md-2 { + .gx-md-2 { --bs-gutter-x: 0.5rem; } - .g-md-2, -.gy-md-2 { + .gy-md-2 { --bs-gutter-y: 0.5rem; } - .g-md-3, -.gx-md-3 { + .gx-md-3 { --bs-gutter-x: 1rem; } - .g-md-3, -.gy-md-3 { + .gy-md-3 { --bs-gutter-y: 1rem; } - .g-md-4, -.gx-md-4 { + .gx-md-4 { --bs-gutter-x: 1.5rem; } - .g-md-4, -.gy-md-4 { + .gy-md-4 { --bs-gutter-y: 1.5rem; } - .g-md-5, -.gx-md-5 { + .gx-md-5 { --bs-gutter-x: 3rem; } - .g-md-5, -.gy-md-5 { + .gy-md-5 { --bs-gutter-y: 3rem; } } @media (min-width: 992px) { .col-lg { - flex: 1 0 0%; + flex: 1 0 0; } - .row-cols-lg-auto > * { flex: 0 0 auto; width: auto; } - .row-cols-lg-1 > * { flex: 0 0 auto; width: 100%; } - .row-cols-lg-2 > * { flex: 0 0 auto; width: 50%; } - .row-cols-lg-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } - .row-cols-lg-4 > * { flex: 0 0 auto; width: 25%; } - .row-cols-lg-5 > * { flex: 0 0 auto; width: 20%; } - .row-cols-lg-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } - .col-lg-auto { flex: 0 0 auto; width: auto; } - .col-lg-1 { flex: 0 0 auto; width: 8.33333333%; } - .col-lg-2 { flex: 0 0 auto; width: 16.66666667%; } - .col-lg-3 { flex: 0 0 auto; width: 25%; } - .col-lg-4 { flex: 0 0 auto; width: 33.33333333%; } - .col-lg-5 { flex: 0 0 auto; width: 41.66666667%; } - .col-lg-6 { flex: 0 0 auto; width: 50%; } - .col-lg-7 { flex: 0 0 auto; width: 58.33333333%; } - .col-lg-8 { flex: 0 0 auto; width: 66.66666667%; } - .col-lg-9 { flex: 0 0 auto; width: 75%; } - .col-lg-10 { flex: 0 0 auto; width: 83.33333333%; } - .col-lg-11 { flex: 0 0 auto; width: 91.66666667%; } - .col-lg-12 { flex: 0 0 auto; width: 100%; } - .offset-lg-0 { margin-left: 0; } - .offset-lg-1 { margin-left: 8.33333333%; } - .offset-lg-2 { margin-left: 16.66666667%; } - .offset-lg-3 { margin-left: 25%; } - .offset-lg-4 { margin-left: 33.33333333%; } - .offset-lg-5 { margin-left: 41.66666667%; } - .offset-lg-6 { margin-left: 50%; } - .offset-lg-7 { margin-left: 58.33333333%; } - .offset-lg-8 { margin-left: 66.66666667%; } - .offset-lg-9 { margin-left: 75%; } - .offset-lg-10 { margin-left: 83.33333333%; } - .offset-lg-11 { margin-left: 91.66666667%; } - .g-lg-0, -.gx-lg-0 { + .gx-lg-0 { --bs-gutter-x: 0; } - .g-lg-0, -.gy-lg-0 { + .gy-lg-0 { --bs-gutter-y: 0; } - .g-lg-1, -.gx-lg-1 { + .gx-lg-1 { --bs-gutter-x: 0.25rem; } - .g-lg-1, -.gy-lg-1 { + .gy-lg-1 { --bs-gutter-y: 0.25rem; } - .g-lg-2, -.gx-lg-2 { + .gx-lg-2 { --bs-gutter-x: 0.5rem; } - .g-lg-2, -.gy-lg-2 { + .gy-lg-2 { --bs-gutter-y: 0.5rem; } - .g-lg-3, -.gx-lg-3 { + .gx-lg-3 { --bs-gutter-x: 1rem; } - .g-lg-3, -.gy-lg-3 { + .gy-lg-3 { --bs-gutter-y: 1rem; } - .g-lg-4, -.gx-lg-4 { + .gx-lg-4 { --bs-gutter-x: 1.5rem; } - .g-lg-4, -.gy-lg-4 { + .gy-lg-4 { --bs-gutter-y: 1.5rem; } - .g-lg-5, -.gx-lg-5 { + .gx-lg-5 { --bs-gutter-x: 3rem; } - .g-lg-5, -.gy-lg-5 { + .gy-lg-5 { --bs-gutter-y: 3rem; } } @media (min-width: 1200px) { .col-xl { - flex: 1 0 0%; + flex: 1 0 0; } - .row-cols-xl-auto > * { flex: 0 0 auto; width: auto; } - .row-cols-xl-1 > * { flex: 0 0 auto; width: 100%; } - .row-cols-xl-2 > * { flex: 0 0 auto; width: 50%; } - .row-cols-xl-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } - .row-cols-xl-4 > * { flex: 0 0 auto; width: 25%; } - .row-cols-xl-5 > * { flex: 0 0 auto; width: 20%; } - .row-cols-xl-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } - .col-xl-auto { flex: 0 0 auto; width: auto; } - .col-xl-1 { flex: 0 0 auto; width: 8.33333333%; } - .col-xl-2 { flex: 0 0 auto; width: 16.66666667%; } - .col-xl-3 { flex: 0 0 auto; width: 25%; } - .col-xl-4 { flex: 0 0 auto; width: 33.33333333%; } - .col-xl-5 { flex: 0 0 auto; width: 41.66666667%; } - .col-xl-6 { flex: 0 0 auto; width: 50%; } - .col-xl-7 { flex: 0 0 auto; width: 58.33333333%; } - .col-xl-8 { flex: 0 0 auto; width: 66.66666667%; } - .col-xl-9 { flex: 0 0 auto; width: 75%; } - .col-xl-10 { flex: 0 0 auto; width: 83.33333333%; } - .col-xl-11 { flex: 0 0 auto; width: 91.66666667%; } - .col-xl-12 { flex: 0 0 auto; width: 100%; } - .offset-xl-0 { margin-left: 0; } - .offset-xl-1 { margin-left: 8.33333333%; } - .offset-xl-2 { margin-left: 16.66666667%; } - .offset-xl-3 { margin-left: 25%; } - .offset-xl-4 { margin-left: 33.33333333%; } - .offset-xl-5 { margin-left: 41.66666667%; } - .offset-xl-6 { margin-left: 50%; } - .offset-xl-7 { margin-left: 58.33333333%; } - .offset-xl-8 { margin-left: 66.66666667%; } - .offset-xl-9 { margin-left: 75%; } - .offset-xl-10 { margin-left: 83.33333333%; } - .offset-xl-11 { margin-left: 91.66666667%; } - .g-xl-0, -.gx-xl-0 { + .gx-xl-0 { --bs-gutter-x: 0; } - .g-xl-0, -.gy-xl-0 { + .gy-xl-0 { --bs-gutter-y: 0; } - .g-xl-1, -.gx-xl-1 { + .gx-xl-1 { --bs-gutter-x: 0.25rem; } - .g-xl-1, -.gy-xl-1 { + .gy-xl-1 { --bs-gutter-y: 0.25rem; } - .g-xl-2, -.gx-xl-2 { + .gx-xl-2 { --bs-gutter-x: 0.5rem; } - .g-xl-2, -.gy-xl-2 { + .gy-xl-2 { --bs-gutter-y: 0.5rem; } - .g-xl-3, -.gx-xl-3 { + .gx-xl-3 { --bs-gutter-x: 1rem; } - .g-xl-3, -.gy-xl-3 { + .gy-xl-3 { --bs-gutter-y: 1rem; } - .g-xl-4, -.gx-xl-4 { + .gx-xl-4 { --bs-gutter-x: 1.5rem; } - .g-xl-4, -.gy-xl-4 { + .gy-xl-4 { --bs-gutter-y: 1.5rem; } - .g-xl-5, -.gx-xl-5 { + .gx-xl-5 { --bs-gutter-x: 3rem; } - .g-xl-5, -.gy-xl-5 { + .gy-xl-5 { --bs-gutter-y: 3rem; } } @media (min-width: 1400px) { .col-xxl { - flex: 1 0 0%; + flex: 1 0 0; } - .row-cols-xxl-auto > * { flex: 0 0 auto; width: auto; } - .row-cols-xxl-1 > * { flex: 0 0 auto; width: 100%; } - .row-cols-xxl-2 > * { flex: 0 0 auto; width: 50%; } - .row-cols-xxl-3 > * { flex: 0 0 auto; - width: 33.3333333333%; + width: 33.33333333%; } - .row-cols-xxl-4 > * { flex: 0 0 auto; width: 25%; } - .row-cols-xxl-5 > * { flex: 0 0 auto; width: 20%; } - .row-cols-xxl-6 > * { flex: 0 0 auto; - width: 16.6666666667%; + width: 16.66666667%; } - .col-xxl-auto { flex: 0 0 auto; width: auto; } - .col-xxl-1 { flex: 0 0 auto; width: 8.33333333%; } - .col-xxl-2 { flex: 0 0 auto; width: 16.66666667%; } - .col-xxl-3 { flex: 0 0 auto; width: 25%; } - .col-xxl-4 { flex: 0 0 auto; width: 33.33333333%; } - .col-xxl-5 { flex: 0 0 auto; width: 41.66666667%; } - .col-xxl-6 { flex: 0 0 auto; width: 50%; } - .col-xxl-7 { flex: 0 0 auto; width: 58.33333333%; } - .col-xxl-8 { flex: 0 0 auto; width: 66.66666667%; } - .col-xxl-9 { flex: 0 0 auto; width: 75%; } - .col-xxl-10 { flex: 0 0 auto; width: 83.33333333%; } - .col-xxl-11 { flex: 0 0 auto; width: 91.66666667%; } - .col-xxl-12 { flex: 0 0 auto; width: 100%; } - .offset-xxl-0 { margin-left: 0; } - .offset-xxl-1 { margin-left: 8.33333333%; } - .offset-xxl-2 { margin-left: 16.66666667%; } - .offset-xxl-3 { margin-left: 25%; } - .offset-xxl-4 { margin-left: 33.33333333%; } - .offset-xxl-5 { margin-left: 41.66666667%; } - .offset-xxl-6 { margin-left: 50%; } - .offset-xxl-7 { margin-left: 58.33333333%; } - .offset-xxl-8 { margin-left: 66.66666667%; } - .offset-xxl-9 { margin-left: 75%; } - .offset-xxl-10 { margin-left: 83.33333333%; } - .offset-xxl-11 { margin-left: 91.66666667%; } - .g-xxl-0, -.gx-xxl-0 { + .gx-xxl-0 { --bs-gutter-x: 0; } - .g-xxl-0, -.gy-xxl-0 { + .gy-xxl-0 { --bs-gutter-y: 0; } - .g-xxl-1, -.gx-xxl-1 { + .gx-xxl-1 { --bs-gutter-x: 0.25rem; } - .g-xxl-1, -.gy-xxl-1 { + .gy-xxl-1 { --bs-gutter-y: 0.25rem; } - .g-xxl-2, -.gx-xxl-2 { + .gx-xxl-2 { --bs-gutter-x: 0.5rem; } - .g-xxl-2, -.gy-xxl-2 { + .gy-xxl-2 { --bs-gutter-y: 0.5rem; } - .g-xxl-3, -.gx-xxl-3 { + .gx-xxl-3 { --bs-gutter-x: 1rem; } - .g-xxl-3, -.gy-xxl-3 { + .gy-xxl-3 { --bs-gutter-y: 1rem; } - .g-xxl-4, -.gx-xxl-4 { + .gx-xxl-4 { --bs-gutter-x: 1.5rem; } - .g-xxl-4, -.gy-xxl-4 { + .gy-xxl-4 { --bs-gutter-y: 1.5rem; } - .g-xxl-5, -.gx-xxl-5 { + .gx-xxl-5 { --bs-gutter-x: 3rem; } - .g-xxl-5, -.gy-xxl-5 { + .gy-xxl-5 { --bs-gutter-y: 3rem; } } .table { - --bs-table-bg: transparent; + --bs-table-color-type: initial; + --bs-table-bg-type: initial; + --bs-table-color-state: initial; + --bs-table-bg-state: initial; + --bs-table-color: initial; + --bs-table-bg: var(--bs-body-bg); + --bs-table-border-color: var(--bs-border-color); --bs-table-accent-bg: transparent; - --bs-table-striped-color: #222; - --bs-table-striped-bg: rgba(0, 0, 0, 0.05); - --bs-table-active-color: #222; - --bs-table-active-bg: rgba(0, 0, 0, 0.1); - --bs-table-hover-color: #222; - --bs-table-hover-bg: rgba(0, 0, 0, 0.075); + --bs-table-striped-color: initial; + --bs-table-striped-bg: rgba(var(--bs-emphasis-color-rgb), 0.05); + --bs-table-active-color: initial; + --bs-table-active-bg: rgba(var(--bs-emphasis-color-rgb), 0.1); + --bs-table-hover-color: initial; + --bs-table-hover-bg: rgba(var(--bs-emphasis-color-rgb), 0.075); width: 100%; margin-bottom: 1rem; - color: #222; vertical-align: top; - border-color: #dee2e6; + border-color: var(--bs-table-border-color); } .table > :not(caption) > * > * { padding: 0.5rem 0.5rem; + color: var(--bs-table-color-state, var(--bs-table-color-type, var(--bs-table-color))); background-color: var(--bs-table-bg); - border-bottom-width: 1px; - box-shadow: inset 0 0 0 9999px var(--bs-table-accent-bg); + border-bottom-width: var(--bs-border-width); + box-shadow: inset 0 0 0 9999px var(--bs-table-bg-state, var(--bs-table-bg-type, var(--bs-table-accent-bg))); } .table > tbody { vertical-align: inherit; @@ -2064,8 +1977,9 @@ progress { .table > thead { vertical-align: bottom; } -.table > :not(:first-child) { - border-top: 2px solid currentColor; + +.table-group-divider { + border-top: calc(var(--bs-border-width) * 2) solid currentcolor; } .caption-top { @@ -2077,10 +1991,10 @@ progress { } .table-bordered > :not(caption) > * { - border-width: 1px 0; + border-width: var(--bs-border-width) 0; } .table-bordered > :not(caption) > * > * { - border-width: 0 1px; + border-width: 0 var(--bs-border-width); } .table-borderless > :not(caption) > * > * { @@ -2091,114 +2005,135 @@ progress { } .table-striped > tbody > tr:nth-of-type(odd) > * { - --bs-table-accent-bg: var(--bs-table-striped-bg); - color: var(--bs-table-striped-color); + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); +} + +.table-striped-columns > :not(caption) > tr > :nth-child(even) { + --bs-table-color-type: var(--bs-table-striped-color); + --bs-table-bg-type: var(--bs-table-striped-bg); } .table-active { - --bs-table-accent-bg: var(--bs-table-active-bg); - color: var(--bs-table-active-color); + --bs-table-color-state: var(--bs-table-active-color); + --bs-table-bg-state: var(--bs-table-active-bg); } .table-hover > tbody > tr:hover > * { - --bs-table-accent-bg: var(--bs-table-hover-bg); - color: var(--bs-table-hover-color); + --bs-table-color-state: var(--bs-table-hover-color); + --bs-table-bg-state: var(--bs-table-hover-bg); } .table-primary { + --bs-table-color: #fff; --bs-table-bg: #158cba; + --bs-table-border-color: #44a3c8; --bs-table-striped-bg: #2192bd; --bs-table-striped-color: #fff; --bs-table-active-bg: #2c98c1; --bs-table-active-color: #fff; --bs-table-hover-bg: #2795bf; --bs-table-hover-color: #fff; - color: #fff; - border-color: #2c98c1; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); } .table-secondary { + --bs-table-color: #000; --bs-table-bg: #f0f0f0; + --bs-table-border-color: silver; --bs-table-striped-bg: #e4e4e4; --bs-table-striped-color: #000; --bs-table-active-bg: #d8d8d8; --bs-table-active-color: #000; --bs-table-hover-bg: #dedede; --bs-table-hover-color: #000; - color: #000; - border-color: #d8d8d8; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); } .table-success { + --bs-table-color: #fff; --bs-table-bg: #28b62c; + --bs-table-border-color: #53c556; --bs-table-striped-bg: #33ba37; --bs-table-striped-color: #fff; --bs-table-active-bg: #3ebd41; --bs-table-active-color: #fff; --bs-table-hover-bg: #38bb3c; --bs-table-hover-color: #fff; - color: #fff; - border-color: #3ebd41; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); } .table-info { + --bs-table-color: #fff; --bs-table-bg: #75caeb; + --bs-table-border-color: #91d5ef; --bs-table-striped-bg: #7ccdec; --bs-table-striped-color: #fff; --bs-table-active-bg: #83cfed; --bs-table-active-color: #000; --bs-table-hover-bg: #7fceed; --bs-table-hover-color: #fff; - color: #fff; - border-color: #83cfed; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); } .table-warning { + --bs-table-color: #fff; --bs-table-bg: #ff851b; + --bs-table-border-color: #ff9d49; --bs-table-striped-bg: #ff8b26; --bs-table-striped-color: #fff; --bs-table-active-bg: #ff9132; --bs-table-active-color: #fff; --bs-table-hover-bg: #ff8e2c; --bs-table-hover-color: #fff; - color: #fff; - border-color: #ff9132; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); } .table-danger { + --bs-table-color: #fff; --bs-table-bg: #ff4136; + --bs-table-border-color: #ff675e; --bs-table-striped-bg: #ff4b40; --bs-table-striped-color: #fff; --bs-table-active-bg: #ff544a; --bs-table-active-color: #fff; --bs-table-hover-bg: #ff4f45; --bs-table-hover-color: #fff; - color: #fff; - border-color: #ff544a; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); } .table-light { + --bs-table-color: #000; --bs-table-bg: #f6f6f6; + --bs-table-border-color: #c5c5c5; --bs-table-striped-bg: #eaeaea; --bs-table-striped-color: #000; --bs-table-active-bg: #dddddd; --bs-table-active-color: #000; --bs-table-hover-bg: #e4e4e4; --bs-table-hover-color: #000; - color: #000; - border-color: #dddddd; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); } .table-dark { + --bs-table-color: #fff; --bs-table-bg: #555; + --bs-table-border-color: #777777; --bs-table-striped-bg: #5e5e5e; --bs-table-striped-color: #fff; --bs-table-active-bg: #666666; --bs-table-active-color: #fff; --bs-table-hover-bg: #626262; --bs-table-hover-color: #fff; - color: #fff; - border-color: #666666; + color: var(--bs-table-color); + border-color: var(--bs-table-border-color); } .table-responsive { @@ -2241,29 +2176,29 @@ progress { } .col-form-label { - padding-top: calc(0.375rem + 1px); - padding-bottom: calc(0.375rem + 1px); + padding-top: calc(0.375rem + var(--bs-border-width)); + padding-bottom: calc(0.375rem + var(--bs-border-width)); margin-bottom: 0; font-size: inherit; line-height: 1.5; } .col-form-label-lg { - padding-top: calc(0.5rem + 1px); - padding-bottom: calc(0.5rem + 1px); + padding-top: calc(0.5rem + var(--bs-border-width)); + padding-bottom: calc(0.5rem + var(--bs-border-width)); font-size: 1.25rem; } .col-form-label-sm { - padding-top: calc(0.25rem + 1px); - padding-bottom: calc(0.25rem + 1px); + padding-top: calc(0.25rem + var(--bs-border-width)); + padding-bottom: calc(0.25rem + var(--bs-border-width)); font-size: 0.875rem; } .form-text { margin-top: 0.25rem; font-size: 0.875em; - color: #999; + color: var(--bs-secondary-color); } .form-control { @@ -2273,14 +2208,14 @@ progress { font-size: 1rem; font-weight: 400; line-height: 1.5; - color: #222; - background-color: #fff; - background-clip: padding-box; - border: 1px solid #ced4da; + color: var(--bs-body-color); -webkit-appearance: none; -moz-appearance: none; appearance: none; - border-radius: 0.25rem; + background-color: var(--bs-body-bg); + background-clip: padding-box; + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { @@ -2295,25 +2230,31 @@ progress { cursor: pointer; } .form-control:focus { - color: #222; - background-color: #fff; + color: var(--bs-body-color); + background-color: var(--bs-body-bg); border-color: #8ac6dd; outline: 0; box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.25); } .form-control::-webkit-date-and-time-value { + min-width: 85px; height: 1.5em; + margin: 0; +} +.form-control::-webkit-datetime-edit { + display: block; + padding: 0; } .form-control::-moz-placeholder { - color: #999; + color: var(--bs-secondary-color); opacity: 1; } .form-control::placeholder { - color: #999; + color: var(--bs-secondary-color); opacity: 1; } -.form-control:disabled, .form-control[readonly] { - background-color: #f0f0f0; +.form-control:disabled { + background-color: var(--bs-secondary-bg); opacity: 1; } .form-control::-webkit-file-upload-button { @@ -2321,13 +2262,13 @@ progress { margin: -0.375rem -0.75rem; -webkit-margin-end: 0.75rem; margin-inline-end: 0.75rem; - color: #222; - background-color: #f0f0f0; + color: var(--bs-body-color); + background-color: var(--bs-tertiary-bg); pointer-events: none; border-color: inherit; border-style: solid; border-width: 0; - border-inline-end-width: 1px; + border-inline-end-width: var(--bs-border-width); border-radius: 0; -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; @@ -2337,13 +2278,13 @@ progress { margin: -0.375rem -0.75rem; -webkit-margin-end: 0.75rem; margin-inline-end: 0.75rem; - color: #222; - background-color: #f0f0f0; + color: var(--bs-body-color); + background-color: var(--bs-tertiary-bg); pointer-events: none; border-color: inherit; border-style: solid; border-width: 0; - border-inline-end-width: 1px; + border-inline-end-width: var(--bs-border-width); border-radius: 0; transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @@ -2357,35 +2298,10 @@ progress { } } .form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { - background-color: #e4e4e4; + background-color: var(--bs-secondary-bg); } .form-control:hover:not(:disabled):not([readonly])::file-selector-button { - background-color: #e4e4e4; -} -.form-control::-webkit-file-upload-button { - padding: 0.375rem 0.75rem; - margin: -0.375rem -0.75rem; - -webkit-margin-end: 0.75rem; - margin-inline-end: 0.75rem; - color: #222; - background-color: #f0f0f0; - pointer-events: none; - border-color: inherit; - border-style: solid; - border-width: 0; - border-inline-end-width: 1px; - border-radius: 0; - -webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; -} -@media (prefers-reduced-motion: reduce) { - .form-control::-webkit-file-upload-button { - -webkit-transition: none; - transition: none; - } -} -.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button { - background-color: #e4e4e4; + background-color: var(--bs-secondary-bg); } .form-control-plaintext { @@ -2394,10 +2310,13 @@ progress { padding: 0.375rem 0; margin-bottom: 0; line-height: 1.5; - color: #222; + color: var(--bs-body-color); background-color: transparent; border: solid transparent; - border-width: 1px 0; + border-width: var(--bs-border-width) 0; +} +.form-control-plaintext:focus { + outline: 0; } .form-control-plaintext.form-control-sm, .form-control-plaintext.form-control-lg { padding-right: 0; @@ -2405,10 +2324,10 @@ progress { } .form-control-sm { - min-height: calc(1.5em + 0.5rem + 2px); + min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); padding: 0.25rem 0.5rem; font-size: 0.875rem; - border-radius: 0.2rem; + border-radius: var(--bs-border-radius-sm); } .form-control-sm::-webkit-file-upload-button { padding: 0.25rem 0.5rem; @@ -2422,18 +2341,12 @@ progress { -webkit-margin-end: 0.5rem; margin-inline-end: 0.5rem; } -.form-control-sm::-webkit-file-upload-button { - padding: 0.25rem 0.5rem; - margin: -0.25rem -0.5rem; - -webkit-margin-end: 0.5rem; - margin-inline-end: 0.5rem; -} .form-control-lg { - min-height: calc(1.5em + 1rem + 2px); + min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); padding: 0.5rem 1rem; font-size: 1.25rem; - border-radius: 0.3rem; + border-radius: var(--bs-border-radius-lg); } .form-control-lg::-webkit-file-upload-button { padding: 0.5rem 1rem; @@ -2447,60 +2360,60 @@ progress { -webkit-margin-end: 1rem; margin-inline-end: 1rem; } -.form-control-lg::-webkit-file-upload-button { - padding: 0.5rem 1rem; - margin: -0.5rem -1rem; - -webkit-margin-end: 1rem; - margin-inline-end: 1rem; -} textarea.form-control { - min-height: calc(1.5em + 0.75rem + 2px); + min-height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2)); } textarea.form-control-sm { - min-height: calc(1.5em + 0.5rem + 2px); + min-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); } textarea.form-control-lg { - min-height: calc(1.5em + 1rem + 2px); + min-height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); } .form-control-color { width: 3rem; - height: auto; + height: calc(1.5em + 0.75rem + calc(var(--bs-border-width) * 2)); padding: 0.375rem; } .form-control-color:not(:disabled):not([readonly]) { cursor: pointer; } .form-control-color::-moz-color-swatch { - height: 1.5em; - border-radius: 0.25rem; + border: 0 !important; + border-radius: var(--bs-border-radius); } .form-control-color::-webkit-color-swatch { - height: 1.5em; - border-radius: 0.25rem; + border: 0 !important; + border-radius: var(--bs-border-radius); +} +.form-control-color.form-control-sm { + height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2)); +} +.form-control-color.form-control-lg { + height: calc(1.5em + 1rem + calc(var(--bs-border-width) * 2)); } .form-select { + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23333' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); display: block; width: 100%; padding: 0.375rem 2.25rem 0.375rem 0.75rem; - -moz-padding-start: calc(0.75rem - 3px); font-size: 1rem; font-weight: 400; line-height: 1.5; - color: #222; - background-color: #fff; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23333' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: right 0.75rem center; - background-size: 16px 12px; - border: 1px solid #ced4da; - border-radius: 0.25rem; - transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + color: var(--bs-body-color); -webkit-appearance: none; -moz-appearance: none; appearance: none; + background-color: var(--bs-body-bg); + background-image: var(--bs-form-select-bg-img), var(--bs-form-select-bg-icon, none); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 16px 12px; + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { .form-select { @@ -2517,11 +2430,11 @@ textarea.form-control-lg { background-image: none; } .form-select:disabled { - background-color: #f0f0f0; + background-color: var(--bs-secondary-bg); } .form-select:-moz-focusring { color: transparent; - text-shadow: 0 0 0 #222; + text-shadow: 0 0 0 var(--bs-body-color); } .form-select-sm { @@ -2529,7 +2442,7 @@ textarea.form-control-lg { padding-bottom: 0.25rem; padding-left: 0.5rem; font-size: 0.875rem; - border-radius: 0.2rem; + border-radius: var(--bs-border-radius-sm); } .form-select-lg { @@ -2537,7 +2450,11 @@ textarea.form-control-lg { padding-bottom: 0.5rem; padding-left: 1rem; font-size: 1.25rem; - border-radius: 0.3rem; + border-radius: var(--bs-border-radius-lg); +} + +[data-bs-theme=dark] .form-select { + --bs-form-select-bg-img: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); } .form-check { @@ -2551,21 +2468,36 @@ textarea.form-control-lg { margin-left: -1.5em; } +.form-check-reverse { + padding-right: 1.5em; + padding-left: 0; + text-align: right; +} +.form-check-reverse .form-check-input { + float: right; + margin-right: -1.5em; + margin-left: 0; +} + .form-check-input { + --bs-form-check-bg: var(--bs-body-bg); + flex-shrink: 0; width: 1em; height: 1em; margin-top: 0.25em; vertical-align: top; - background-color: #fff; - background-repeat: no-repeat; - background-position: center; - background-size: contain; - border: 1px solid rgba(0, 0, 0, 0.25); -webkit-appearance: none; -moz-appearance: none; appearance: none; + background-color: var(--bs-form-check-bg); + background-image: var(--bs-form-check-bg-image); + background-repeat: no-repeat; + background-position: center; + background-size: contain; + border: var(--bs-border-width) solid var(--bs-border-color); -webkit-print-color-adjust: exact; color-adjust: exact; + print-color-adjust: exact; } .form-check-input[type=checkbox] { border-radius: 0.25em; @@ -2586,15 +2518,15 @@ textarea.form-control-lg { border-color: #158cba; } .form-check-input:checked[type=checkbox] { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e"); + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e"); } .form-check-input:checked[type=radio] { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e"); } .form-check-input[type=checkbox]:indeterminate { background-color: #158cba; border-color: #158cba; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); + --bs-form-check-bg-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e"); } .form-check-input:disabled { pointer-events: none; @@ -2602,6 +2534,7 @@ textarea.form-control-lg { opacity: 0.5; } .form-check-input[disabled] ~ .form-check-label, .form-check-input:disabled ~ .form-check-label { + cursor: default; opacity: 0.5; } @@ -2609,9 +2542,10 @@ textarea.form-control-lg { padding-left: 2.5em; } .form-switch .form-check-input { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); width: 2em; margin-left: -2.5em; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e"); + background-image: var(--bs-form-switch-bg); background-position: left center; border-radius: 2em; transition: background-position 0.15s ease-in-out; @@ -2622,11 +2556,19 @@ textarea.form-control-lg { } } .form-switch .form-check-input:focus { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%238ac6dd'/%3e%3c/svg%3e"); + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%238ac6dd'/%3e%3c/svg%3e"); } .form-switch .form-check-input:checked { background-position: right center; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e"); +} +.form-switch.form-check-reverse { + padding-right: 2.5em; + padding-left: 0; +} +.form-switch.form-check-reverse .form-check-input { + margin-right: -2.5em; + margin-left: 0; } .form-check-inline { @@ -2645,14 +2587,18 @@ textarea.form-control-lg { opacity: 0.65; } +[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus) { + --bs-form-switch-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e"); +} + .form-range { width: 100%; height: 1.5rem; padding: 0; - background-color: transparent; -webkit-appearance: none; -moz-appearance: none; appearance: none; + background-color: transparent; } .form-range:focus { outline: 0; @@ -2670,13 +2616,13 @@ textarea.form-control-lg { width: 1rem; height: 1rem; margin-top: -0.25rem; + -webkit-appearance: none; + appearance: none; background-color: #158cba; border: 0; border-radius: 1rem; -webkit-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - -webkit-appearance: none; - appearance: none; } @media (prefers-reduced-motion: reduce) { .form-range::-webkit-slider-thumb { @@ -2692,20 +2638,20 @@ textarea.form-control-lg { height: 0.5rem; color: transparent; cursor: pointer; - background-color: #dee2e6; + background-color: var(--bs-secondary-bg); border-color: transparent; border-radius: 1rem; } .form-range::-moz-range-thumb { width: 1rem; height: 1rem; + -moz-appearance: none; + appearance: none; background-color: #158cba; border: 0; border-radius: 1rem; -moz-transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; - -moz-appearance: none; - appearance: none; } @media (prefers-reduced-motion: reduce) { .form-range::-moz-range-thumb { @@ -2721,7 +2667,7 @@ textarea.form-control-lg { height: 0.5rem; color: transparent; cursor: pointer; - background-color: #dee2e6; + background-color: var(--bs-secondary-bg); border-color: transparent; border-radius: 1rem; } @@ -2729,28 +2675,37 @@ textarea.form-control-lg { pointer-events: none; } .form-range:disabled::-webkit-slider-thumb { - background-color: #adb5bd; + background-color: var(--bs-secondary-color); } .form-range:disabled::-moz-range-thumb { - background-color: #adb5bd; + background-color: var(--bs-secondary-color); } .form-floating { position: relative; } .form-floating > .form-control, +.form-floating > .form-control-plaintext, .form-floating > .form-select { - height: calc(3.5rem + 2px); + height: calc(3.5rem + calc(var(--bs-border-width) * 2)); + min-height: calc(3.5rem + calc(var(--bs-border-width) * 2)); line-height: 1.25; } .form-floating > label { position: absolute; top: 0; left: 0; + z-index: 2; + max-width: 100%; height: 100%; padding: 1rem 0.75rem; + overflow: hidden; + color: rgba(var(--bs-body-color-rgb), 0.65); + text-align: start; + text-overflow: ellipsis; + white-space: nowrap; pointer-events: none; - border: 1px solid transparent; + border: var(--bs-border-width) solid transparent; transform-origin: 0 0; transition: opacity 0.1s ease-in-out, transform 0.1s ease-in-out; } @@ -2759,45 +2714,78 @@ textarea.form-control-lg { transition: none; } } -.form-floating > .form-control { +.form-floating > .form-control, +.form-floating > .form-control-plaintext { padding: 1rem 0.75rem; } -.form-floating > .form-control::-moz-placeholder { +.form-floating > .form-control::-moz-placeholder, .form-floating > .form-control-plaintext::-moz-placeholder { color: transparent; } -.form-floating > .form-control::placeholder { +.form-floating > .form-control::placeholder, +.form-floating > .form-control-plaintext::placeholder { color: transparent; } -.form-floating > .form-control:not(:-moz-placeholder-shown) { +.form-floating > .form-control:not(:-moz-placeholder-shown), .form-floating > .form-control-plaintext:not(:-moz-placeholder-shown) { padding-top: 1.625rem; padding-bottom: 0.625rem; } -.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown) { +.form-floating > .form-control:focus, .form-floating > .form-control:not(:placeholder-shown), +.form-floating > .form-control-plaintext:focus, +.form-floating > .form-control-plaintext:not(:placeholder-shown) { padding-top: 1.625rem; padding-bottom: 0.625rem; } -.form-floating > .form-control:-webkit-autofill { +.form-floating > .form-control:-webkit-autofill, +.form-floating > .form-control-plaintext:-webkit-autofill { padding-top: 1.625rem; padding-bottom: 0.625rem; } .form-floating > .form-select { padding-top: 1.625rem; padding-bottom: 0.625rem; + padding-left: 0.75rem; } .form-floating > .form-control:not(:-moz-placeholder-shown) ~ label { - opacity: 0.65; transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } .form-floating > .form-control:focus ~ label, .form-floating > .form-control:not(:placeholder-shown) ~ label, +.form-floating > .form-control-plaintext ~ label, .form-floating > .form-select ~ label { - opacity: 0.65; transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } .form-floating > .form-control:-webkit-autofill ~ label { - opacity: 0.65; transform: scale(0.85) translateY(-0.5rem) translateX(0.15rem); } +.form-floating > textarea:not(:-moz-placeholder-shown) ~ label::after { + position: absolute; + inset: 1rem 0.375rem; + z-index: -1; + height: 1.5em; + content: ""; + background-color: var(--bs-body-bg); + border-radius: var(--bs-border-radius); +} +.form-floating > textarea:focus ~ label::after, +.form-floating > textarea:not(:placeholder-shown) ~ label::after { + position: absolute; + inset: 1rem 0.375rem; + z-index: -1; + height: 1.5em; + content: ""; + background-color: var(--bs-body-bg); + border-radius: var(--bs-border-radius); +} +.form-floating > textarea:disabled ~ label::after { + background-color: var(--bs-secondary-bg); +} +.form-floating > .form-control-plaintext ~ label { + border-width: var(--bs-border-width) 0; +} +.form-floating > :disabled ~ label, +.form-floating > .form-control:disabled ~ label { + color: #999; +} .input-group { position: relative; @@ -2807,22 +2795,24 @@ textarea.form-control-lg { width: 100%; } .input-group > .form-control, -.input-group > .form-select { +.input-group > .form-select, +.input-group > .form-floating { position: relative; flex: 1 1 auto; width: 1%; min-width: 0; } .input-group > .form-control:focus, -.input-group > .form-select:focus { - z-index: 3; +.input-group > .form-select:focus, +.input-group > .form-floating:focus-within { + z-index: 5; } .input-group .btn { position: relative; z-index: 2; } .input-group .btn:focus { - z-index: 3; + z-index: 5; } .input-group-text { @@ -2832,12 +2822,12 @@ textarea.form-control-lg { font-size: 1rem; font-weight: 400; line-height: 1.5; - color: #222; + color: var(--bs-body-color); text-align: center; white-space: nowrap; - background-color: #f0f0f0; - border: 1px solid #ced4da; - border-radius: 0.25rem; + background-color: var(--bs-tertiary-bg); + border: var(--bs-border-width) solid var(--bs-border-color); + border-radius: var(--bs-border-radius); } .input-group-lg > .form-control, @@ -2846,7 +2836,7 @@ textarea.form-control-lg { .input-group-lg > .btn { padding: 0.5rem 1rem; font-size: 1.25rem; - border-radius: 0.3rem; + border-radius: var(--bs-border-radius-lg); } .input-group-sm > .form-control, @@ -2855,7 +2845,7 @@ textarea.form-control-lg { .input-group-sm > .btn { padding: 0.25rem 0.5rem; font-size: 0.875rem; - border-radius: 0.2rem; + border-radius: var(--bs-border-radius-sm); } .input-group-lg > .form-select, @@ -2863,18 +2853,27 @@ textarea.form-control-lg { padding-right: 3rem; } -.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu), -.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3) { +.input-group:not(.has-validation) > :not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), +.input-group:not(.has-validation) > .dropdown-toggle:nth-last-child(n+3), +.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-control, +.input-group:not(.has-validation) > .form-floating:not(:last-child) > .form-select { border-top-right-radius: 0; border-bottom-right-radius: 0; } -.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu), -.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4) { +.input-group.has-validation > :nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating), +.input-group.has-validation > .dropdown-toggle:nth-last-child(n+4), +.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-control, +.input-group.has-validation > .form-floating:nth-last-child(n+3) > .form-select { border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) { - margin-left: -1px; + margin-left: calc(-1 * var(--bs-border-width)); + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group > .form-floating:not(:first-child) > .form-control, +.input-group > .form-floating:not(:first-child) > .form-select { border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -2884,7 +2883,7 @@ textarea.form-control-lg { width: 100%; margin-top: 0.25rem; font-size: 0.875em; - color: #28b62c; + color: var(--bs-form-valid-color); } .valid-tooltip { @@ -2897,8 +2896,8 @@ textarea.form-control-lg { margin-top: 0.1rem; font-size: 0.875rem; color: #fff; - background-color: rgba(40, 182, 44, 0.9); - border-radius: 0.25rem; + background-color: var(--bs-success); + border-radius: var(--bs-border-radius); } .was-validated :valid ~ .valid-feedback, @@ -2909,16 +2908,16 @@ textarea.form-control-lg { } .was-validated .form-control:valid, .form-control.is-valid { - border-color: #28b62c; + border-color: var(--bs-form-valid-border-color); padding-right: calc(1.5em + 0.75rem); - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328b62c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328b62c' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e"); background-repeat: no-repeat; background-position: right calc(0.375em + 0.1875rem) center; background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .was-validated .form-control:valid:focus, .form-control.is-valid:focus { - border-color: #28b62c; - box-shadow: 0 0 0 0.25rem rgba(40, 182, 44, 0.25); + border-color: var(--bs-form-valid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); } .was-validated textarea.form-control:valid, textarea.form-control.is-valid { @@ -2927,44 +2926,45 @@ textarea.form-control-lg { } .was-validated .form-select:valid, .form-select.is-valid { - border-color: #28b62c; + border-color: var(--bs-form-valid-border-color); } .was-validated .form-select:valid:not([multiple]):not([size]), .was-validated .form-select:valid:not([multiple])[size="1"], .form-select.is-valid:not([multiple]):not([size]), .form-select.is-valid:not([multiple])[size="1"] { + --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328b62c' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e"); padding-right: 4.125rem; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23333' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%2328b62c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e"); background-position: right 0.75rem center, center right 2.25rem; background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .was-validated .form-select:valid:focus, .form-select.is-valid:focus { - border-color: #28b62c; - box-shadow: 0 0 0 0.25rem rgba(40, 182, 44, 0.25); + border-color: var(--bs-form-valid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); +} + +.was-validated .form-control-color:valid, .form-control-color.is-valid { + width: calc(3rem + calc(1.5em + 0.75rem)); } .was-validated .form-check-input:valid, .form-check-input.is-valid { - border-color: #28b62c; + border-color: var(--bs-form-valid-border-color); } .was-validated .form-check-input:valid:checked, .form-check-input.is-valid:checked { - background-color: #28b62c; + background-color: var(--bs-form-valid-color); } .was-validated .form-check-input:valid:focus, .form-check-input.is-valid:focus { - box-shadow: 0 0 0 0.25rem rgba(40, 182, 44, 0.25); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-success-rgb), 0.25); } .was-validated .form-check-input:valid ~ .form-check-label, .form-check-input.is-valid ~ .form-check-label { - color: #28b62c; + color: var(--bs-form-valid-color); } .form-check-inline .form-check-input ~ .valid-feedback { margin-left: 0.5em; } -.was-validated .input-group .form-control:valid, .input-group .form-control.is-valid, -.was-validated .input-group .form-select:valid, -.input-group .form-select.is-valid { - z-index: 1; -} -.was-validated .input-group .form-control:valid:focus, .input-group .form-control.is-valid:focus, -.was-validated .input-group .form-select:valid:focus, -.input-group .form-select.is-valid:focus { +.was-validated .input-group > .form-control:not(:focus):valid, .input-group > .form-control:not(:focus).is-valid, +.was-validated .input-group > .form-select:not(:focus):valid, +.input-group > .form-select:not(:focus).is-valid, +.was-validated .input-group > .form-floating:not(:focus-within):valid, +.input-group > .form-floating:not(:focus-within).is-valid { z-index: 3; } @@ -2973,7 +2973,7 @@ textarea.form-control-lg { width: 100%; margin-top: 0.25rem; font-size: 0.875em; - color: #ff4136; + color: var(--bs-form-invalid-color); } .invalid-tooltip { @@ -2986,8 +2986,8 @@ textarea.form-control-lg { margin-top: 0.1rem; font-size: 0.875rem; color: #fff; - background-color: rgba(255, 65, 54, 0.9); - border-radius: 0.25rem; + background-color: var(--bs-danger); + border-radius: var(--bs-border-radius); } .was-validated :invalid ~ .invalid-feedback, @@ -2998,7 +2998,7 @@ textarea.form-control-lg { } .was-validated .form-control:invalid, .form-control.is-invalid { - border-color: #ff4136; + border-color: var(--bs-form-invalid-border-color); padding-right: calc(1.5em + 0.75rem); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff4136'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff4136' stroke='none'/%3e%3c/svg%3e"); background-repeat: no-repeat; @@ -3006,8 +3006,8 @@ textarea.form-control-lg { background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .was-validated .form-control:invalid:focus, .form-control.is-invalid:focus { - border-color: #ff4136; - box-shadow: 0 0 0 0.25rem rgba(255, 65, 54, 0.25); + border-color: var(--bs-form-invalid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); } .was-validated textarea.form-control:invalid, textarea.form-control.is-invalid { @@ -3016,52 +3016,71 @@ textarea.form-control-lg { } .was-validated .form-select:invalid, .form-select.is-invalid { - border-color: #ff4136; + border-color: var(--bs-form-invalid-border-color); } .was-validated .form-select:invalid:not([multiple]):not([size]), .was-validated .form-select:invalid:not([multiple])[size="1"], .form-select.is-invalid:not([multiple]):not([size]), .form-select.is-invalid:not([multiple])[size="1"] { + --bs-form-select-bg-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff4136'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff4136' stroke='none'/%3e%3c/svg%3e"); padding-right: 4.125rem; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23333' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"), url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23ff4136'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23ff4136' stroke='none'/%3e%3c/svg%3e"); background-position: right 0.75rem center, center right 2.25rem; background-size: 16px 12px, calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } .was-validated .form-select:invalid:focus, .form-select.is-invalid:focus { - border-color: #ff4136; - box-shadow: 0 0 0 0.25rem rgba(255, 65, 54, 0.25); + border-color: var(--bs-form-invalid-border-color); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); +} + +.was-validated .form-control-color:invalid, .form-control-color.is-invalid { + width: calc(3rem + calc(1.5em + 0.75rem)); } .was-validated .form-check-input:invalid, .form-check-input.is-invalid { - border-color: #ff4136; + border-color: var(--bs-form-invalid-border-color); } .was-validated .form-check-input:invalid:checked, .form-check-input.is-invalid:checked { - background-color: #ff4136; + background-color: var(--bs-form-invalid-color); } .was-validated .form-check-input:invalid:focus, .form-check-input.is-invalid:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 65, 54, 0.25); + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), 0.25); } .was-validated .form-check-input:invalid ~ .form-check-label, .form-check-input.is-invalid ~ .form-check-label { - color: #ff4136; + color: var(--bs-form-invalid-color); } .form-check-inline .form-check-input ~ .invalid-feedback { margin-left: 0.5em; } -.was-validated .input-group .form-control:invalid, .input-group .form-control.is-invalid, -.was-validated .input-group .form-select:invalid, -.input-group .form-select.is-invalid { - z-index: 2; -} -.was-validated .input-group .form-control:invalid:focus, .input-group .form-control.is-invalid:focus, -.was-validated .input-group .form-select:invalid:focus, -.input-group .form-select.is-invalid:focus { - z-index: 3; +.was-validated .input-group > .form-control:not(:focus):invalid, .input-group > .form-control:not(:focus).is-invalid, +.was-validated .input-group > .form-select:not(:focus):invalid, +.input-group > .form-select:not(:focus).is-invalid, +.was-validated .input-group > .form-floating:not(:focus-within):invalid, +.input-group > .form-floating:not(:focus-within).is-invalid { + z-index: 4; } .btn { + --bs-btn-padding-x: 0.75rem; + --bs-btn-padding-y: 0.375rem; + --bs-btn-font-family: ; + --bs-btn-font-size: 1rem; + --bs-btn-font-weight: 700; + --bs-btn-line-height: 1.5; + --bs-btn-color: var(--bs-body-color); + --bs-btn-bg: transparent; + --bs-btn-border-width: var(--bs-border-width); + --bs-btn-border-color: transparent; + --bs-btn-border-radius: var(--bs-border-radius); + --bs-btn-hover-border-color: transparent; + --bs-btn-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075); + --bs-btn-disabled-opacity: 0.65; + --bs-btn-focus-box-shadow: 0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5); display: inline-block; - font-weight: 700; - line-height: 1.5; - color: #222; + padding: var(--bs-btn-padding-y) var(--bs-btn-padding-x); + font-family: var(--bs-btn-font-family); + font-size: var(--bs-btn-font-size); + font-weight: var(--bs-btn-font-weight); + line-height: var(--bs-btn-line-height); + color: var(--bs-btn-color); text-align: center; text-decoration: none; vertical-align: middle; @@ -3069,11 +3088,9 @@ textarea.form-control-lg { -webkit-user-select: none; -moz-user-select: none; user-select: none; - background-color: transparent; - border: 1px solid transparent; - padding: 0.375rem 0.75rem; - font-size: 1rem; - border-radius: 0.25rem; + border: var(--bs-btn-border-width) solid var(--bs-btn-border-color); + border-radius: var(--bs-btn-border-radius); + background-color: var(--bs-btn-bg); transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { @@ -3082,479 +3099,352 @@ textarea.form-control-lg { } } .btn:hover { - color: #222; + color: var(--bs-btn-hover-color); + background-color: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); } -.btn-check:focus + .btn, .btn:focus { +.btn-check + .btn:hover { + color: var(--bs-btn-color); + background-color: var(--bs-btn-bg); + border-color: var(--bs-btn-border-color); +} +.btn:focus-visible { + color: var(--bs-btn-hover-color); + background-color: var(--bs-btn-hover-bg); + border-color: var(--bs-btn-hover-border-color); outline: 0; - box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.25); + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:focus-visible + .btn { + border-color: var(--bs-btn-hover-border-color); + outline: 0; + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:checked + .btn, :not(.btn-check) + .btn:active, .btn:first-child:active, .btn.active, .btn.show { + color: var(--bs-btn-active-color); + background-color: var(--bs-btn-active-bg); + border-color: var(--bs-btn-active-border-color); +} +.btn-check:checked + .btn:focus-visible, :not(.btn-check) + .btn:active:focus-visible, .btn:first-child:active:focus-visible, .btn.active:focus-visible, .btn.show:focus-visible { + box-shadow: var(--bs-btn-focus-box-shadow); +} +.btn-check:checked:focus-visible + .btn { + box-shadow: var(--bs-btn-focus-box-shadow); } .btn:disabled, .btn.disabled, fieldset:disabled .btn { + color: var(--bs-btn-disabled-color); pointer-events: none; - opacity: 0.65; + background-color: var(--bs-btn-disabled-bg); + border-color: var(--bs-btn-disabled-border-color); + opacity: var(--bs-btn-disabled-opacity); } .btn-primary { - color: #fff; - background-color: #158cba; - border-color: #158cba; -} -.btn-primary:hover { - color: #fff; - background-color: #12779e; - border-color: #117095; -} -.btn-check:focus + .btn-primary, .btn-primary:focus { - color: #fff; - background-color: #12779e; - border-color: #117095; - box-shadow: 0 0 0 0.25rem rgba(56, 157, 196, 0.5); -} -.btn-check:checked + .btn-primary, .btn-check:active + .btn-primary, .btn-primary:active, .btn-primary.active, .show > .btn-primary.dropdown-toggle { - color: #fff; - background-color: #117095; - border-color: #10698c; -} -.btn-check:checked + .btn-primary:focus, .btn-check:active + .btn-primary:focus, .btn-primary:active:focus, .btn-primary.active:focus, .show > .btn-primary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(56, 157, 196, 0.5); -} -.btn-primary:disabled, .btn-primary.disabled { - color: #fff; - background-color: #158cba; - border-color: #158cba; + --bs-btn-color: #fff; + --bs-btn-bg: #158cba; + --bs-btn-border-color: #158cba; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #12779e; + --bs-btn-hover-border-color: #117095; + --bs-btn-focus-shadow-rgb: 56, 157, 196; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #117095; + --bs-btn-active-border-color: #10698c; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #158cba; + --bs-btn-disabled-border-color: #158cba; } .btn-secondary { - color: #000; - background-color: #f0f0f0; - border-color: #f0f0f0; -} -.btn-secondary:hover { - color: #000; - background-color: #f2f2f2; - border-color: #f2f2f2; -} -.btn-check:focus + .btn-secondary, .btn-secondary:focus { - color: #000; - background-color: #f2f2f2; - border-color: #f2f2f2; - box-shadow: 0 0 0 0.25rem rgba(204, 204, 204, 0.5); -} -.btn-check:checked + .btn-secondary, .btn-check:active + .btn-secondary, .btn-secondary:active, .btn-secondary.active, .show > .btn-secondary.dropdown-toggle { - color: #000; - background-color: #f3f3f3; - border-color: #f2f2f2; -} -.btn-check:checked + .btn-secondary:focus, .btn-check:active + .btn-secondary:focus, .btn-secondary:active:focus, .btn-secondary.active:focus, .show > .btn-secondary.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(204, 204, 204, 0.5); -} -.btn-secondary:disabled, .btn-secondary.disabled { - color: #000; - background-color: #f0f0f0; - border-color: #f0f0f0; + --bs-btn-color: #000; + --bs-btn-bg: #f0f0f0; + --bs-btn-border-color: #f0f0f0; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #f2f2f2; + --bs-btn-hover-border-color: #f2f2f2; + --bs-btn-focus-shadow-rgb: 204, 204, 204; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #f3f3f3; + --bs-btn-active-border-color: #f2f2f2; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #f0f0f0; + --bs-btn-disabled-border-color: #f0f0f0; } .btn-success { - color: #fff; - background-color: #28b62c; - border-color: #28b62c; -} -.btn-success:hover { - color: #fff; - background-color: #229b25; - border-color: #209223; -} -.btn-check:focus + .btn-success, .btn-success:focus { - color: #fff; - background-color: #229b25; - border-color: #209223; - box-shadow: 0 0 0 0.25rem rgba(72, 193, 76, 0.5); -} -.btn-check:checked + .btn-success, .btn-check:active + .btn-success, .btn-success:active, .btn-success.active, .show > .btn-success.dropdown-toggle { - color: #fff; - background-color: #209223; - border-color: #1e8921; -} -.btn-check:checked + .btn-success:focus, .btn-check:active + .btn-success:focus, .btn-success:active:focus, .btn-success.active:focus, .show > .btn-success.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(72, 193, 76, 0.5); -} -.btn-success:disabled, .btn-success.disabled { - color: #fff; - background-color: #28b62c; - border-color: #28b62c; + --bs-btn-color: #fff; + --bs-btn-bg: #28b62c; + --bs-btn-border-color: #28b62c; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #229b25; + --bs-btn-hover-border-color: #209223; + --bs-btn-focus-shadow-rgb: 72, 193, 76; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #209223; + --bs-btn-active-border-color: #1e8921; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #28b62c; + --bs-btn-disabled-border-color: #28b62c; } .btn-info { - color: #fff; - background-color: #75caeb; - border-color: #75caeb; -} -.btn-info:hover { - color: #fff; - background-color: #63acc8; - border-color: #5ea2bc; -} -.btn-check:focus + .btn-info, .btn-info:focus { - color: #fff; - background-color: #63acc8; - border-color: #5ea2bc; - box-shadow: 0 0 0 0.25rem rgba(138, 210, 238, 0.5); -} -.btn-check:checked + .btn-info, .btn-check:active + .btn-info, .btn-info:active, .btn-info.active, .show > .btn-info.dropdown-toggle { - color: #fff; - background-color: #5ea2bc; - border-color: #5898b0; -} -.btn-check:checked + .btn-info:focus, .btn-check:active + .btn-info:focus, .btn-info:active:focus, .btn-info.active:focus, .show > .btn-info.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(138, 210, 238, 0.5); -} -.btn-info:disabled, .btn-info.disabled { - color: #fff; - background-color: #75caeb; - border-color: #75caeb; + --bs-btn-color: #fff; + --bs-btn-bg: #75caeb; + --bs-btn-border-color: #75caeb; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #63acc8; + --bs-btn-hover-border-color: #5ea2bc; + --bs-btn-focus-shadow-rgb: 138, 210, 238; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #5ea2bc; + --bs-btn-active-border-color: #5898b0; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #75caeb; + --bs-btn-disabled-border-color: #75caeb; } .btn-warning { - color: #fff; - background-color: #ff851b; - border-color: #ff851b; -} -.btn-warning:hover { - color: #fff; - background-color: #d97117; - border-color: #cc6a16; -} -.btn-check:focus + .btn-warning, .btn-warning:focus { - color: #fff; - background-color: #d97117; - border-color: #cc6a16; - box-shadow: 0 0 0 0.25rem rgba(255, 151, 61, 0.5); -} -.btn-check:checked + .btn-warning, .btn-check:active + .btn-warning, .btn-warning:active, .btn-warning.active, .show > .btn-warning.dropdown-toggle { - color: #fff; - background-color: #cc6a16; - border-color: #bf6414; -} -.btn-check:checked + .btn-warning:focus, .btn-check:active + .btn-warning:focus, .btn-warning:active:focus, .btn-warning.active:focus, .show > .btn-warning.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 151, 61, 0.5); -} -.btn-warning:disabled, .btn-warning.disabled { - color: #fff; - background-color: #ff851b; - border-color: #ff851b; + --bs-btn-color: #fff; + --bs-btn-bg: #ff851b; + --bs-btn-border-color: #ff851b; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #d97117; + --bs-btn-hover-border-color: #cc6a16; + --bs-btn-focus-shadow-rgb: 255, 151, 61; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #cc6a16; + --bs-btn-active-border-color: #bf6414; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #ff851b; + --bs-btn-disabled-border-color: #ff851b; } .btn-danger { - color: #fff; - background-color: #ff4136; - border-color: #ff4136; -} -.btn-danger:hover { - color: #fff; - background-color: #d9372e; - border-color: #cc342b; -} -.btn-check:focus + .btn-danger, .btn-danger:focus { - color: #fff; - background-color: #d9372e; - border-color: #cc342b; - box-shadow: 0 0 0 0.25rem rgba(255, 94, 84, 0.5); -} -.btn-check:checked + .btn-danger, .btn-check:active + .btn-danger, .btn-danger:active, .btn-danger.active, .show > .btn-danger.dropdown-toggle { - color: #fff; - background-color: #cc342b; - border-color: #bf3129; -} -.btn-check:checked + .btn-danger:focus, .btn-check:active + .btn-danger:focus, .btn-danger:active:focus, .btn-danger.active:focus, .show > .btn-danger.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 94, 84, 0.5); -} -.btn-danger:disabled, .btn-danger.disabled { - color: #fff; - background-color: #ff4136; - border-color: #ff4136; + --bs-btn-color: #fff; + --bs-btn-bg: #ff4136; + --bs-btn-border-color: #ff4136; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #d9372e; + --bs-btn-hover-border-color: #cc342b; + --bs-btn-focus-shadow-rgb: 255, 94, 84; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #cc342b; + --bs-btn-active-border-color: #bf3129; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #ff4136; + --bs-btn-disabled-border-color: #ff4136; } .btn-light { - color: #000; - background-color: #f6f6f6; - border-color: #f6f6f6; -} -.btn-light:hover { - color: #000; - background-color: #f7f7f7; - border-color: #f7f7f7; -} -.btn-check:focus + .btn-light, .btn-light:focus { - color: #000; - background-color: #f7f7f7; - border-color: #f7f7f7; - box-shadow: 0 0 0 0.25rem rgba(209, 209, 209, 0.5); -} -.btn-check:checked + .btn-light, .btn-check:active + .btn-light, .btn-light:active, .btn-light.active, .show > .btn-light.dropdown-toggle { - color: #000; - background-color: #f8f8f8; - border-color: #f7f7f7; -} -.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 { - box-shadow: 0 0 0 0.25rem rgba(209, 209, 209, 0.5); -} -.btn-light:disabled, .btn-light.disabled { - color: #000; - background-color: #f6f6f6; - border-color: #f6f6f6; + --bs-btn-color: #000; + --bs-btn-bg: #f6f6f6; + --bs-btn-border-color: #f6f6f6; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #d1d1d1; + --bs-btn-hover-border-color: #c5c5c5; + --bs-btn-focus-shadow-rgb: 209, 209, 209; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #c5c5c5; + --bs-btn-active-border-color: #b9b9b9; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #000; + --bs-btn-disabled-bg: #f6f6f6; + --bs-btn-disabled-border-color: #f6f6f6; } .btn-dark { - color: #fff; - background-color: #555; - border-color: #555; -} -.btn-dark:hover { - color: #fff; - background-color: #484848; - border-color: #444444; -} -.btn-check:focus + .btn-dark, .btn-dark:focus { - color: #fff; - background-color: #484848; - border-color: #444444; - box-shadow: 0 0 0 0.25rem rgba(111, 111, 111, 0.5); -} -.btn-check:checked + .btn-dark, .btn-check:active + .btn-dark, .btn-dark:active, .btn-dark.active, .show > .btn-dark.dropdown-toggle { - color: #fff; - background-color: #444444; - border-color: #404040; -} -.btn-check:checked + .btn-dark:focus, .btn-check:active + .btn-dark:focus, .btn-dark:active:focus, .btn-dark.active:focus, .show > .btn-dark.dropdown-toggle:focus { - box-shadow: 0 0 0 0.25rem rgba(111, 111, 111, 0.5); -} -.btn-dark:disabled, .btn-dark.disabled { - color: #fff; - background-color: #555; - border-color: #555; + --bs-btn-color: #fff; + --bs-btn-bg: #555; + --bs-btn-border-color: #555; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #6f6f6f; + --bs-btn-hover-border-color: #666666; + --bs-btn-focus-shadow-rgb: 111, 111, 111; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #777777; + --bs-btn-active-border-color: #666666; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #fff; + --bs-btn-disabled-bg: #555; + --bs-btn-disabled-border-color: #555; } .btn-outline-primary { - color: #158cba; - border-color: #158cba; -} -.btn-outline-primary:hover { - color: #fff; - background-color: #158cba; - border-color: #158cba; -} -.btn-check:focus + .btn-outline-primary, .btn-outline-primary:focus { - box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.5); -} -.btn-check:checked + .btn-outline-primary, .btn-check:active + .btn-outline-primary, .btn-outline-primary:active, .btn-outline-primary.active, .btn-outline-primary.dropdown-toggle.show { - color: #fff; - background-color: #158cba; - border-color: #158cba; -} -.btn-check:checked + .btn-outline-primary:focus, .btn-check:active + .btn-outline-primary:focus, .btn-outline-primary:active:focus, .btn-outline-primary.active:focus, .btn-outline-primary.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.5); -} -.btn-outline-primary:disabled, .btn-outline-primary.disabled { - color: #158cba; - background-color: transparent; + --bs-btn-color: #158cba; + --bs-btn-border-color: #158cba; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #158cba; + --bs-btn-hover-border-color: #158cba; + --bs-btn-focus-shadow-rgb: 21, 140, 186; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #158cba; + --bs-btn-active-border-color: #158cba; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #158cba; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #158cba; + --bs-gradient: none; } .btn-outline-secondary { - color: #f0f0f0; - border-color: #f0f0f0; -} -.btn-outline-secondary:hover { - color: #000; - background-color: #f0f0f0; - border-color: #f0f0f0; -} -.btn-check:focus + .btn-outline-secondary, .btn-outline-secondary:focus { - box-shadow: 0 0 0 0.25rem rgba(240, 240, 240, 0.5); -} -.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 { - color: #000; - background-color: #f0f0f0; - border-color: #f0f0f0; -} -.btn-check:checked + .btn-outline-secondary:focus, .btn-check:active + .btn-outline-secondary:focus, .btn-outline-secondary:active:focus, .btn-outline-secondary.active:focus, .btn-outline-secondary.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(240, 240, 240, 0.5); -} -.btn-outline-secondary:disabled, .btn-outline-secondary.disabled { - color: #f0f0f0; - background-color: transparent; + --bs-btn-color: #f0f0f0; + --bs-btn-border-color: #f0f0f0; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #f0f0f0; + --bs-btn-hover-border-color: #f0f0f0; + --bs-btn-focus-shadow-rgb: 240, 240, 240; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #f0f0f0; + --bs-btn-active-border-color: #f0f0f0; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #f0f0f0; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #f0f0f0; + --bs-gradient: none; } .btn-outline-success { - color: #28b62c; - border-color: #28b62c; -} -.btn-outline-success:hover { - color: #fff; - background-color: #28b62c; - border-color: #28b62c; -} -.btn-check:focus + .btn-outline-success, .btn-outline-success:focus { - box-shadow: 0 0 0 0.25rem rgba(40, 182, 44, 0.5); -} -.btn-check:checked + .btn-outline-success, .btn-check:active + .btn-outline-success, .btn-outline-success:active, .btn-outline-success.active, .btn-outline-success.dropdown-toggle.show { - color: #fff; - background-color: #28b62c; - border-color: #28b62c; -} -.btn-check:checked + .btn-outline-success:focus, .btn-check:active + .btn-outline-success:focus, .btn-outline-success:active:focus, .btn-outline-success.active:focus, .btn-outline-success.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(40, 182, 44, 0.5); -} -.btn-outline-success:disabled, .btn-outline-success.disabled { - color: #28b62c; - background-color: transparent; + --bs-btn-color: #28b62c; + --bs-btn-border-color: #28b62c; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #28b62c; + --bs-btn-hover-border-color: #28b62c; + --bs-btn-focus-shadow-rgb: 40, 182, 44; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #28b62c; + --bs-btn-active-border-color: #28b62c; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #28b62c; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #28b62c; + --bs-gradient: none; } .btn-outline-info { - color: #75caeb; - border-color: #75caeb; -} -.btn-outline-info:hover { - color: #fff; - background-color: #75caeb; - border-color: #75caeb; -} -.btn-check:focus + .btn-outline-info, .btn-outline-info:focus { - box-shadow: 0 0 0 0.25rem rgba(117, 202, 235, 0.5); -} -.btn-check:checked + .btn-outline-info, .btn-check:active + .btn-outline-info, .btn-outline-info:active, .btn-outline-info.active, .btn-outline-info.dropdown-toggle.show { - color: #fff; - background-color: #75caeb; - border-color: #75caeb; -} -.btn-check:checked + .btn-outline-info:focus, .btn-check:active + .btn-outline-info:focus, .btn-outline-info:active:focus, .btn-outline-info.active:focus, .btn-outline-info.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(117, 202, 235, 0.5); -} -.btn-outline-info:disabled, .btn-outline-info.disabled { - color: #75caeb; - background-color: transparent; + --bs-btn-color: #75caeb; + --bs-btn-border-color: #75caeb; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #75caeb; + --bs-btn-hover-border-color: #75caeb; + --bs-btn-focus-shadow-rgb: 117, 202, 235; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #75caeb; + --bs-btn-active-border-color: #75caeb; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #75caeb; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #75caeb; + --bs-gradient: none; } .btn-outline-warning { - color: #ff851b; - border-color: #ff851b; -} -.btn-outline-warning:hover { - color: #fff; - background-color: #ff851b; - border-color: #ff851b; -} -.btn-check:focus + .btn-outline-warning, .btn-outline-warning:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 133, 27, 0.5); -} -.btn-check:checked + .btn-outline-warning, .btn-check:active + .btn-outline-warning, .btn-outline-warning:active, .btn-outline-warning.active, .btn-outline-warning.dropdown-toggle.show { - color: #fff; - background-color: #ff851b; - border-color: #ff851b; -} -.btn-check:checked + .btn-outline-warning:focus, .btn-check:active + .btn-outline-warning:focus, .btn-outline-warning:active:focus, .btn-outline-warning.active:focus, .btn-outline-warning.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 133, 27, 0.5); -} -.btn-outline-warning:disabled, .btn-outline-warning.disabled { - color: #ff851b; - background-color: transparent; + --bs-btn-color: #ff851b; + --bs-btn-border-color: #ff851b; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #ff851b; + --bs-btn-hover-border-color: #ff851b; + --bs-btn-focus-shadow-rgb: 255, 133, 27; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #ff851b; + --bs-btn-active-border-color: #ff851b; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #ff851b; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #ff851b; + --bs-gradient: none; } .btn-outline-danger { - color: #ff4136; - border-color: #ff4136; -} -.btn-outline-danger:hover { - color: #fff; - background-color: #ff4136; - border-color: #ff4136; -} -.btn-check:focus + .btn-outline-danger, .btn-outline-danger:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 65, 54, 0.5); -} -.btn-check:checked + .btn-outline-danger, .btn-check:active + .btn-outline-danger, .btn-outline-danger:active, .btn-outline-danger.active, .btn-outline-danger.dropdown-toggle.show { - color: #fff; - background-color: #ff4136; - border-color: #ff4136; -} -.btn-check:checked + .btn-outline-danger:focus, .btn-check:active + .btn-outline-danger:focus, .btn-outline-danger:active:focus, .btn-outline-danger.active:focus, .btn-outline-danger.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(255, 65, 54, 0.5); -} -.btn-outline-danger:disabled, .btn-outline-danger.disabled { - color: #ff4136; - background-color: transparent; + --bs-btn-color: #ff4136; + --bs-btn-border-color: #ff4136; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #ff4136; + --bs-btn-hover-border-color: #ff4136; + --bs-btn-focus-shadow-rgb: 255, 65, 54; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #ff4136; + --bs-btn-active-border-color: #ff4136; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #ff4136; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #ff4136; + --bs-gradient: none; } .btn-outline-light { - color: #f6f6f6; - border-color: #f6f6f6; -} -.btn-outline-light:hover { - color: #000; - background-color: #f6f6f6; - border-color: #f6f6f6; -} -.btn-check:focus + .btn-outline-light, .btn-outline-light:focus { - box-shadow: 0 0 0 0.25rem rgba(246, 246, 246, 0.5); -} -.btn-check:checked + .btn-outline-light, .btn-check:active + .btn-outline-light, .btn-outline-light:active, .btn-outline-light.active, .btn-outline-light.dropdown-toggle.show { - color: #000; - background-color: #f6f6f6; - border-color: #f6f6f6; -} -.btn-check:checked + .btn-outline-light:focus, .btn-check:active + .btn-outline-light:focus, .btn-outline-light:active:focus, .btn-outline-light.active:focus, .btn-outline-light.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(246, 246, 246, 0.5); -} -.btn-outline-light:disabled, .btn-outline-light.disabled { - color: #f6f6f6; - background-color: transparent; + --bs-btn-color: #f6f6f6; + --bs-btn-border-color: #f6f6f6; + --bs-btn-hover-color: #000; + --bs-btn-hover-bg: #f6f6f6; + --bs-btn-hover-border-color: #f6f6f6; + --bs-btn-focus-shadow-rgb: 246, 246, 246; + --bs-btn-active-color: #000; + --bs-btn-active-bg: #f6f6f6; + --bs-btn-active-border-color: #f6f6f6; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #f6f6f6; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #f6f6f6; + --bs-gradient: none; } .btn-outline-dark { - color: #555; - border-color: #555; -} -.btn-outline-dark:hover { - color: #fff; - background-color: #555; - border-color: #555; -} -.btn-check:focus + .btn-outline-dark, .btn-outline-dark:focus { - box-shadow: 0 0 0 0.25rem rgba(85, 85, 85, 0.5); -} -.btn-check:checked + .btn-outline-dark, .btn-check:active + .btn-outline-dark, .btn-outline-dark:active, .btn-outline-dark.active, .btn-outline-dark.dropdown-toggle.show { - color: #fff; - background-color: #555; - border-color: #555; -} -.btn-check:checked + .btn-outline-dark:focus, .btn-check:active + .btn-outline-dark:focus, .btn-outline-dark:active:focus, .btn-outline-dark.active:focus, .btn-outline-dark.dropdown-toggle.show:focus { - box-shadow: 0 0 0 0.25rem rgba(85, 85, 85, 0.5); -} -.btn-outline-dark:disabled, .btn-outline-dark.disabled { - color: #555; - background-color: transparent; + --bs-btn-color: #555; + --bs-btn-border-color: #555; + --bs-btn-hover-color: #fff; + --bs-btn-hover-bg: #555; + --bs-btn-hover-border-color: #555; + --bs-btn-focus-shadow-rgb: 85, 85, 85; + --bs-btn-active-color: #fff; + --bs-btn-active-bg: #555; + --bs-btn-active-border-color: #555; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-color: #555; + --bs-btn-disabled-bg: transparent; + --bs-btn-disabled-border-color: #555; + --bs-gradient: none; } .btn-link { - font-weight: 400; - color: #158cba; + --bs-btn-font-weight: 400; + --bs-btn-color: var(--bs-link-color); + --bs-btn-bg: transparent; + --bs-btn-border-color: transparent; + --bs-btn-hover-color: var(--bs-link-hover-color); + --bs-btn-hover-border-color: transparent; + --bs-btn-active-color: var(--bs-link-hover-color); + --bs-btn-active-border-color: transparent; + --bs-btn-disabled-color: #999; + --bs-btn-disabled-border-color: transparent; + --bs-btn-box-shadow: 0 0 0 #000; + --bs-btn-focus-shadow-rgb: 56, 157, 196; text-decoration: underline; } -.btn-link:hover { - color: #117095; +.btn-link:focus-visible { + color: var(--bs-btn-color); } -.btn-link:disabled, .btn-link.disabled { - color: #999; +.btn-link:hover { + color: var(--bs-btn-hover-color); } .btn-lg, .btn-group-lg > .btn { - padding: 0.5rem 1rem; - font-size: 1.25rem; - border-radius: 0.3rem; + --bs-btn-padding-y: 0.5rem; + --bs-btn-padding-x: 1rem; + --bs-btn-font-size: 1.25rem; + --bs-btn-border-radius: var(--bs-border-radius-lg); } .btn-sm, .btn-group-sm > .btn { - padding: 0.25rem 0.5rem; - font-size: 0.875rem; - border-radius: 0.2rem; + --bs-btn-padding-y: 0.25rem; + --bs-btn-padding-x: 0.5rem; + --bs-btn-font-size: 0.875rem; + --bs-btn-border-radius: var(--bs-border-radius-sm); } .fade { @@ -3597,7 +3487,9 @@ textarea.form-control-lg { .dropup, .dropend, .dropdown, -.dropstart { +.dropstart, +.dropup-center, +.dropdown-center { position: relative; } @@ -3619,25 +3511,51 @@ textarea.form-control-lg { } .dropdown-menu { + --bs-dropdown-zindex: 1000; + --bs-dropdown-min-width: 10rem; + --bs-dropdown-padding-x: 0; + --bs-dropdown-padding-y: 0.5rem; + --bs-dropdown-spacer: 0.125rem; + --bs-dropdown-font-size: 1rem; + --bs-dropdown-color: var(--bs-body-color); + --bs-dropdown-bg: var(--bs-body-bg); + --bs-dropdown-border-color: var(--bs-border-color-translucent); + --bs-dropdown-border-radius: var(--bs-border-radius); + --bs-dropdown-border-width: var(--bs-border-width); + --bs-dropdown-inner-border-radius: calc(var(--bs-border-radius) - var(--bs-border-width)); + --bs-dropdown-divider-bg: var(--bs-border-color-translucent); + --bs-dropdown-divider-margin-y: 0.5rem; + --bs-dropdown-box-shadow: var(--bs-box-shadow); + --bs-dropdown-link-color: var(--bs-tertiary-color); + --bs-dropdown-link-hover-color: var(--bs-tertiary-color); + --bs-dropdown-link-hover-bg: var(--bs-tertiary-bg); + --bs-dropdown-link-active-color: #fff; + --bs-dropdown-link-active-bg: #158cba; + --bs-dropdown-link-disabled-color: var(--bs-tertiary-color); + --bs-dropdown-item-padding-x: 1rem; + --bs-dropdown-item-padding-y: 0.25rem; + --bs-dropdown-header-color: #999; + --bs-dropdown-header-padding-x: 1rem; + --bs-dropdown-header-padding-y: 0.5rem; position: absolute; - z-index: 1000; + z-index: var(--bs-dropdown-zindex); display: none; - min-width: 10rem; - padding: 0.5rem 0; + min-width: var(--bs-dropdown-min-width); + padding: var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x); margin: 0; - font-size: 1rem; - color: #222; + font-size: var(--bs-dropdown-font-size); + color: var(--bs-dropdown-color); text-align: left; list-style: none; - background-color: #fff; + background-color: var(--bs-dropdown-bg); background-clip: padding-box; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 0.25rem; + border: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); + border-radius: var(--bs-dropdown-border-radius); } .dropdown-menu[data-bs-popper] { top: 100%; left: 0; - margin-top: 0.125rem; + margin-top: var(--bs-dropdown-spacer); } .dropdown-menu-start { @@ -3664,7 +3582,6 @@ textarea.form-control-lg { right: auto; left: 0; } - .dropdown-menu-sm-end { --bs-position: end; } @@ -3681,7 +3598,6 @@ textarea.form-control-lg { right: auto; left: 0; } - .dropdown-menu-md-end { --bs-position: end; } @@ -3698,7 +3614,6 @@ textarea.form-control-lg { right: auto; left: 0; } - .dropdown-menu-lg-end { --bs-position: end; } @@ -3715,7 +3630,6 @@ textarea.form-control-lg { right: auto; left: 0; } - .dropdown-menu-xl-end { --bs-position: end; } @@ -3732,7 +3646,6 @@ textarea.form-control-lg { right: auto; left: 0; } - .dropdown-menu-xxl-end { --bs-position: end; } @@ -3745,7 +3658,7 @@ textarea.form-control-lg { top: auto; bottom: 100%; margin-top: 0; - margin-bottom: 0.125rem; + margin-bottom: var(--bs-dropdown-spacer); } .dropup .dropdown-toggle::after { display: inline-block; @@ -3766,7 +3679,7 @@ textarea.form-control-lg { right: auto; left: 100%; margin-top: 0; - margin-left: 0.125rem; + margin-left: var(--bs-dropdown-spacer); } .dropend .dropdown-toggle::after { display: inline-block; @@ -3790,7 +3703,7 @@ textarea.form-control-lg { right: 100%; left: auto; margin-top: 0; - margin-right: 0.125rem; + margin-right: var(--bs-dropdown-spacer); } .dropstart .dropdown-toggle::after { display: inline-block; @@ -3819,35 +3732,37 @@ textarea.form-control-lg { .dropdown-divider { height: 0; - margin: 0.5rem 0; + margin: var(--bs-dropdown-divider-margin-y) 0; overflow: hidden; - border-top: 1px solid rgba(0, 0, 0, 0.15); + border-top: 1px solid var(--bs-dropdown-divider-bg); + opacity: 1; } .dropdown-item { display: block; width: 100%; - padding: 0.25rem 1rem; + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); clear: both; font-weight: 400; - color: rgba(0, 0, 0, 0.5); + color: var(--bs-dropdown-link-color); text-align: inherit; text-decoration: none; white-space: nowrap; background-color: transparent; border: 0; + border-radius: var(--bs-dropdown-item-border-radius, 0); } .dropdown-item:hover, .dropdown-item:focus { - color: rgba(0, 0, 0, 0.55); - background-color: #f0f0f0; + color: var(--bs-dropdown-link-hover-color); + background-color: var(--bs-dropdown-link-hover-bg); } .dropdown-item.active, .dropdown-item:active { - color: #fff; + color: var(--bs-dropdown-link-active-color); text-decoration: none; - background-color: #158cba; + background-color: var(--bs-dropdown-link-active-bg); } .dropdown-item.disabled, .dropdown-item:disabled { - color: #adb5bd; + color: var(--bs-dropdown-link-disabled-color); pointer-events: none; background-color: transparent; } @@ -3858,46 +3773,32 @@ textarea.form-control-lg { .dropdown-header { display: block; - padding: 0.5rem 1rem; + padding: var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x); margin-bottom: 0; font-size: 0.875rem; - color: #999; + color: var(--bs-dropdown-header-color); white-space: nowrap; } .dropdown-item-text { display: block; - padding: 0.25rem 1rem; - color: rgba(0, 0, 0, 0.5); + padding: var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x); + color: var(--bs-dropdown-link-color); } .dropdown-menu-dark { - color: #dee2e6; - background-color: #333; - border-color: rgba(0, 0, 0, 0.15); -} -.dropdown-menu-dark .dropdown-item { - color: #dee2e6; -} -.dropdown-menu-dark .dropdown-item:hover, .dropdown-menu-dark .dropdown-item:focus { - color: #fff; - background-color: rgba(255, 255, 255, 0.15); -} -.dropdown-menu-dark .dropdown-item.active, .dropdown-menu-dark .dropdown-item:active { - color: #fff; - background-color: #158cba; -} -.dropdown-menu-dark .dropdown-item.disabled, .dropdown-menu-dark .dropdown-item:disabled { - color: #adb5bd; -} -.dropdown-menu-dark .dropdown-divider { - border-color: rgba(0, 0, 0, 0.15); -} -.dropdown-menu-dark .dropdown-item-text { - color: #dee2e6; -} -.dropdown-menu-dark .dropdown-header { - color: #adb5bd; + --bs-dropdown-color: #dee2e6; + --bs-dropdown-bg: #333; + --bs-dropdown-border-color: var(--bs-border-color-translucent); + --bs-dropdown-box-shadow: ; + --bs-dropdown-link-color: #dee2e6; + --bs-dropdown-link-hover-color: #fff; + --bs-dropdown-divider-bg: var(--bs-border-color-translucent); + --bs-dropdown-link-hover-bg: rgba(255, 255, 255, 0.15); + --bs-dropdown-link-active-color: #fff; + --bs-dropdown-link-active-bg: #158cba; + --bs-dropdown-link-disabled-color: #adb5bd; + --bs-dropdown-header-color: #adb5bd; } .btn-group, @@ -3935,11 +3836,15 @@ textarea.form-control-lg { width: auto; } -.btn-group > .btn:not(:first-child), +.btn-group { + border-radius: var(--bs-border-radius); +} +.btn-group > :not(.btn-check:first-child) + .btn, .btn-group > .btn-group:not(:first-child) { - margin-left: -1px; + margin-left: calc(-1 * var(--bs-border-width)); } .btn-group > .btn:not(:last-child):not(.dropdown-toggle), +.btn-group > .btn.dropdown-toggle-split:first-child, .btn-group > .btn-group:not(:last-child) > .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; @@ -3983,20 +3888,27 @@ textarea.form-control-lg { } .btn-group-vertical > .btn:not(:first-child), .btn-group-vertical > .btn-group:not(:first-child) { - margin-top: -1px; + margin-top: calc(-1 * var(--bs-border-width)); } .btn-group-vertical > .btn:not(:last-child):not(.dropdown-toggle), .btn-group-vertical > .btn-group:not(:last-child) > .btn { border-bottom-right-radius: 0; border-bottom-left-radius: 0; } -.btn-group-vertical > .btn ~ .btn, +.btn-group-vertical > .btn:nth-child(n+3), +.btn-group-vertical > :not(.btn-check) + .btn, .btn-group-vertical > .btn-group:not(:first-child) > .btn { border-top-left-radius: 0; border-top-right-radius: 0; } .nav { + --bs-nav-link-padding-x: 1rem; + --bs-nav-link-padding-y: 0.5rem; + --bs-nav-link-font-weight: ; + --bs-nav-link-color: var(--bs-link-color); + --bs-nav-link-hover-color: var(--bs-link-hover-color); + --bs-nav-link-disabled-color: var(--bs-secondary-color); display: flex; flex-wrap: wrap; padding-left: 0; @@ -4006,9 +3918,13 @@ textarea.form-control-lg { .nav-link { display: block; - padding: 0.5rem 1rem; - color: #158cba; + padding: var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x); + font-size: var(--bs-nav-link-font-size); + font-weight: var(--bs-nav-link-font-weight); + color: var(--bs-nav-link-color); text-decoration: none; + background: none; + border: 0; transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { @@ -4017,54 +3933,83 @@ textarea.form-control-lg { } } .nav-link:hover, .nav-link:focus { - color: #117095; + color: var(--bs-nav-link-hover-color); } -.nav-link.disabled { - color: #999; +.nav-link:focus-visible { + outline: 0; + box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.25); +} +.nav-link.disabled, .nav-link:disabled { + color: var(--bs-nav-link-disabled-color); pointer-events: none; cursor: default; } .nav-tabs { - border-bottom: 1px solid #f0f0f0; + --bs-nav-tabs-border-width: var(--bs-border-width); + --bs-nav-tabs-border-color: var(--bs-border-color); + --bs-nav-tabs-border-radius: var(--bs-border-radius); + --bs-nav-tabs-link-hover-border-color: var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color); + --bs-nav-tabs-link-active-color: var(--bs-emphasis-color); + --bs-nav-tabs-link-active-bg: var(--bs-body-bg); + --bs-nav-tabs-link-active-border-color: var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg); + border-bottom: var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color); } .nav-tabs .nav-link { - margin-bottom: -1px; - background: none; - border: 1px solid transparent; - border-top-left-radius: 0.25rem; - border-top-right-radius: 0.25rem; + margin-bottom: calc(-1 * var(--bs-nav-tabs-border-width)); + border: var(--bs-nav-tabs-border-width) solid transparent; + border-top-left-radius: var(--bs-nav-tabs-border-radius); + border-top-right-radius: var(--bs-nav-tabs-border-radius); } .nav-tabs .nav-link:hover, .nav-tabs .nav-link:focus { - border-color: #f0f0f0; isolation: isolate; -} -.nav-tabs .nav-link.disabled { - color: #999; - background-color: transparent; - border-color: transparent; + border-color: var(--bs-nav-tabs-link-hover-border-color); } .nav-tabs .nav-link.active, .nav-tabs .nav-item.show .nav-link { - color: #222; - background-color: #fff; - border-color: #f0f0f0; + color: var(--bs-nav-tabs-link-active-color); + background-color: var(--bs-nav-tabs-link-active-bg); + border-color: var(--bs-nav-tabs-link-active-border-color); } .nav-tabs .dropdown-menu { - margin-top: -1px; + margin-top: calc(-1 * var(--bs-nav-tabs-border-width)); border-top-left-radius: 0; border-top-right-radius: 0; } +.nav-pills { + --bs-nav-pills-border-radius: var(--bs-border-radius); + --bs-nav-pills-link-active-color: #fff; + --bs-nav-pills-link-active-bg: #158cba; +} .nav-pills .nav-link { - background: none; - border: 0; - border-radius: 0.25rem; + border-radius: var(--bs-nav-pills-border-radius); } .nav-pills .nav-link.active, .nav-pills .show > .nav-link { - color: #fff; - background-color: #158cba; + color: var(--bs-nav-pills-link-active-color); + background-color: var(--bs-nav-pills-link-active-bg); +} + +.nav-underline { + --bs-nav-underline-gap: 1rem; + --bs-nav-underline-border-width: 0.125rem; + --bs-nav-underline-link-active-color: var(--bs-emphasis-color); + gap: var(--bs-nav-underline-gap); +} +.nav-underline .nav-link { + padding-right: 0; + padding-left: 0; + border-bottom: var(--bs-nav-underline-border-width) solid transparent; +} +.nav-underline .nav-link:hover, .nav-underline .nav-link:focus { + border-bottom-color: currentcolor; +} +.nav-underline .nav-link.active, +.nav-underline .show > .nav-link { + font-weight: 700; + color: var(--bs-nav-underline-link-active-color); + border-bottom-color: currentcolor; } .nav-fill > .nav-link, @@ -4075,8 +4020,8 @@ textarea.form-control-lg { .nav-justified > .nav-link, .nav-justified .nav-item { - flex-basis: 0; flex-grow: 1; + flex-basis: 0; text-align: center; } @@ -4093,13 +4038,32 @@ textarea.form-control-lg { } .navbar { + --bs-navbar-padding-x: 0; + --bs-navbar-padding-y: 0.5rem; + --bs-navbar-color: rgba(var(--bs-emphasis-color-rgb), 0.65); + --bs-navbar-hover-color: rgba(var(--bs-emphasis-color-rgb), 0.8); + --bs-navbar-disabled-color: rgba(var(--bs-emphasis-color-rgb), 0.3); + --bs-navbar-active-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-brand-padding-y: 0.3125rem; + --bs-navbar-brand-margin-end: 1rem; + --bs-navbar-brand-font-size: 1.25rem; + --bs-navbar-brand-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-brand-hover-color: rgba(var(--bs-emphasis-color-rgb), 1); + --bs-navbar-nav-link-padding-x: 0.5rem; + --bs-navbar-toggler-padding-y: 0.25rem; + --bs-navbar-toggler-padding-x: 0.75rem; + --bs-navbar-toggler-font-size: 1.25rem; + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2834, 34, 34, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); + --bs-navbar-toggler-border-color: rgba(var(--bs-emphasis-color-rgb), 0.15); + --bs-navbar-toggler-border-radius: var(--bs-border-radius); + --bs-navbar-toggler-focus-width: 0.25rem; + --bs-navbar-toggler-transition: box-shadow 0.15s ease-in-out; position: relative; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; - padding-top: 0.5rem; - padding-bottom: 0.5rem; + padding: var(--bs-navbar-padding-y) var(--bs-navbar-padding-x); } .navbar > .container, .navbar > .container-fluid, @@ -4114,23 +4078,33 @@ textarea.form-control-lg { justify-content: space-between; } .navbar-brand { - padding-top: 0.3125rem; - padding-bottom: 0.3125rem; - margin-right: 1rem; - font-size: 1.25rem; + padding-top: var(--bs-navbar-brand-padding-y); + padding-bottom: var(--bs-navbar-brand-padding-y); + margin-right: var(--bs-navbar-brand-margin-end); + font-size: var(--bs-navbar-brand-font-size); + color: var(--bs-navbar-brand-color); text-decoration: none; white-space: nowrap; } +.navbar-brand:hover, .navbar-brand:focus { + color: var(--bs-navbar-brand-hover-color); +} + .navbar-nav { + --bs-nav-link-padding-x: 0; + --bs-nav-link-padding-y: 0.5rem; + --bs-nav-link-font-weight: ; + --bs-nav-link-color: var(--bs-navbar-color); + --bs-nav-link-hover-color: var(--bs-navbar-hover-color); + --bs-nav-link-disabled-color: var(--bs-navbar-disabled-color); display: flex; flex-direction: column; padding-left: 0; margin-bottom: 0; list-style: none; } -.navbar-nav .nav-link { - padding-right: 0; - padding-left: 0; +.navbar-nav .nav-link.active, .navbar-nav .nav-link.show { + color: var(--bs-navbar-active-color); } .navbar-nav .dropdown-menu { position: static; @@ -4139,22 +4113,29 @@ textarea.form-control-lg { .navbar-text { padding-top: 0.5rem; padding-bottom: 0.5rem; + color: var(--bs-navbar-color); +} +.navbar-text a, +.navbar-text a:hover, +.navbar-text a:focus { + color: var(--bs-navbar-active-color); } .navbar-collapse { - flex-basis: 100%; flex-grow: 1; + flex-basis: 100%; align-items: center; } .navbar-toggler { - padding: 0.25rem 0.75rem; - font-size: 1.25rem; + padding: var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x); + font-size: var(--bs-navbar-toggler-font-size); line-height: 1; + color: var(--bs-navbar-color); background-color: transparent; - border: 1px solid transparent; - border-radius: 0.25rem; - transition: box-shadow 0.15s ease-in-out; + border: var(--bs-border-width) solid var(--bs-navbar-toggler-border-color); + border-radius: var(--bs-navbar-toggler-border-radius); + transition: var(--bs-navbar-toggler-transition); } @media (prefers-reduced-motion: reduce) { .navbar-toggler { @@ -4167,7 +4148,7 @@ textarea.form-control-lg { .navbar-toggler:focus { text-decoration: none; outline: 0; - box-shadow: 0 0 0 0.25rem; + box-shadow: 0 0 0 var(--bs-navbar-toggler-focus-width); } .navbar-toggler-icon { @@ -4175,6 +4156,7 @@ textarea.form-control-lg { width: 1.5em; height: 1.5em; vertical-align: middle; + background-image: var(--bs-navbar-toggler-icon-bg); background-repeat: no-repeat; background-position: center; background-size: 100%; @@ -4197,8 +4179,8 @@ textarea.form-control-lg { position: absolute; } .navbar-expand-sm .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); } .navbar-expand-sm .navbar-nav-scroll { overflow: visible; @@ -4210,28 +4192,22 @@ textarea.form-control-lg { .navbar-expand-sm .navbar-toggler { display: none; } - .navbar-expand-sm .offcanvas-header { + .navbar-expand-sm .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-sm .offcanvas .offcanvas-header { display: none; } - .navbar-expand-sm .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-sm .offcanvas-top, -.navbar-expand-sm .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-sm .offcanvas-body { + .navbar-expand-sm .offcanvas .offcanvas-body { display: flex; flex-grow: 0; padding: 0; @@ -4250,8 +4226,8 @@ textarea.form-control-lg { position: absolute; } .navbar-expand-md .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); } .navbar-expand-md .navbar-nav-scroll { overflow: visible; @@ -4263,28 +4239,22 @@ textarea.form-control-lg { .navbar-expand-md .navbar-toggler { display: none; } - .navbar-expand-md .offcanvas-header { + .navbar-expand-md .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-md .offcanvas .offcanvas-header { display: none; } - .navbar-expand-md .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-md .offcanvas-top, -.navbar-expand-md .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-md .offcanvas-body { + .navbar-expand-md .offcanvas .offcanvas-body { display: flex; flex-grow: 0; padding: 0; @@ -4303,8 +4273,8 @@ textarea.form-control-lg { position: absolute; } .navbar-expand-lg .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); } .navbar-expand-lg .navbar-nav-scroll { overflow: visible; @@ -4316,28 +4286,22 @@ textarea.form-control-lg { .navbar-expand-lg .navbar-toggler { display: none; } - .navbar-expand-lg .offcanvas-header { + .navbar-expand-lg .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-lg .offcanvas .offcanvas-header { display: none; } - .navbar-expand-lg .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-lg .offcanvas-top, -.navbar-expand-lg .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-lg .offcanvas-body { + .navbar-expand-lg .offcanvas .offcanvas-body { display: flex; flex-grow: 0; padding: 0; @@ -4356,8 +4320,8 @@ textarea.form-control-lg { position: absolute; } .navbar-expand-xl .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); } .navbar-expand-xl .navbar-nav-scroll { overflow: visible; @@ -4369,28 +4333,22 @@ textarea.form-control-lg { .navbar-expand-xl .navbar-toggler { display: none; } - .navbar-expand-xl .offcanvas-header { + .navbar-expand-xl .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-xl .offcanvas .offcanvas-header { display: none; } - .navbar-expand-xl .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-xl .offcanvas-top, -.navbar-expand-xl .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-xl .offcanvas-body { + .navbar-expand-xl .offcanvas .offcanvas-body { display: flex; flex-grow: 0; padding: 0; @@ -4409,8 +4367,8 @@ textarea.form-control-lg { position: absolute; } .navbar-expand-xxl .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); } .navbar-expand-xxl .navbar-nav-scroll { overflow: visible; @@ -4422,28 +4380,22 @@ textarea.form-control-lg { .navbar-expand-xxl .navbar-toggler { display: none; } - .navbar-expand-xxl .offcanvas-header { + .navbar-expand-xxl .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; + } + .navbar-expand-xxl .offcanvas .offcanvas-header { display: none; } - .navbar-expand-xxl .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; - } - .navbar-expand-xxl .offcanvas-top, -.navbar-expand-xxl .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; - } - .navbar-expand-xxl .offcanvas-body { + .navbar-expand-xxl .offcanvas .offcanvas-body { display: flex; flex-grow: 0; padding: 0; @@ -4461,8 +4413,8 @@ textarea.form-control-lg { position: absolute; } .navbar-expand .navbar-nav .nav-link { - padding-right: 0.5rem; - padding-left: 0.5rem; + padding-right: var(--bs-navbar-nav-link-padding-x); + padding-left: var(--bs-navbar-nav-link-padding-x); } .navbar-expand .navbar-nav-scroll { overflow: visible; @@ -4474,114 +4426,75 @@ textarea.form-control-lg { .navbar-expand .navbar-toggler { display: none; } -.navbar-expand .offcanvas-header { +.navbar-expand .offcanvas { + position: static; + z-index: auto; + flex-grow: 1; + width: auto !important; + height: auto !important; + visibility: visible !important; + background-color: transparent !important; + border: 0 !important; + transform: none !important; + transition: none; +} +.navbar-expand .offcanvas .offcanvas-header { display: none; } -.navbar-expand .offcanvas { - position: inherit; - bottom: 0; - z-index: 1000; - flex-grow: 1; - visibility: visible !important; - background-color: transparent; - border-right: 0; - border-left: 0; - transition: none; - transform: none; -} -.navbar-expand .offcanvas-top, -.navbar-expand .offcanvas-bottom { - height: auto; - border-top: 0; - border-bottom: 0; -} -.navbar-expand .offcanvas-body { +.navbar-expand .offcanvas .offcanvas-body { display: flex; flex-grow: 0; padding: 0; overflow-y: visible; } -.navbar-light .navbar-brand { - color: rgba(0, 0, 0, 0.9); -} -.navbar-light .navbar-brand:hover, .navbar-light .navbar-brand:focus { - color: rgba(0, 0, 0, 0.9); -} -.navbar-light .navbar-nav .nav-link { - color: rgba(0, 0, 0, 0.55); -} -.navbar-light .navbar-nav .nav-link:hover, .navbar-light .navbar-nav .nav-link:focus { - color: rgba(0, 0, 0, 0.7); -} -.navbar-light .navbar-nav .nav-link.disabled { - color: rgba(0, 0, 0, 0.3); -} -.navbar-light .navbar-nav .show > .nav-link, -.navbar-light .navbar-nav .nav-link.active { - color: rgba(0, 0, 0, 0.9); -} -.navbar-light .navbar-toggler { - color: rgba(0, 0, 0, 0.55); - border-color: rgba(0, 0, 0, 0.1); -} -.navbar-light .navbar-toggler-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); -} -.navbar-light .navbar-text { - color: rgba(0, 0, 0, 0.55); -} -.navbar-light .navbar-text a, -.navbar-light .navbar-text a:hover, -.navbar-light .navbar-text a:focus { - color: rgba(0, 0, 0, 0.9); +.navbar-dark, +.navbar[data-bs-theme=dark] { + --bs-navbar-color: rgba(255, 255, 255, 0.55); + --bs-navbar-hover-color: rgba(255, 255, 255, 0.75); + --bs-navbar-disabled-color: rgba(255, 255, 255, 0.25); + --bs-navbar-active-color: #fff; + --bs-navbar-brand-color: #fff; + --bs-navbar-brand-hover-color: #fff; + --bs-navbar-toggler-border-color: rgba(255, 255, 255, 0.1); + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } -.navbar-dark .navbar-brand { - color: #fff; -} -.navbar-dark .navbar-brand:hover, .navbar-dark .navbar-brand:focus { - color: #fff; -} -.navbar-dark .navbar-nav .nav-link { - color: rgba(255, 255, 255, 0.55); -} -.navbar-dark .navbar-nav .nav-link:hover, .navbar-dark .navbar-nav .nav-link:focus { - color: rgba(255, 255, 255, 0.75); -} -.navbar-dark .navbar-nav .nav-link.disabled { - color: rgba(255, 255, 255, 0.25); -} -.navbar-dark .navbar-nav .show > .nav-link, -.navbar-dark .navbar-nav .nav-link.active { - color: #fff; -} -.navbar-dark .navbar-toggler { - color: rgba(255, 255, 255, 0.55); - border-color: rgba(255, 255, 255, 0.1); -} -.navbar-dark .navbar-toggler-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); -} -.navbar-dark .navbar-text { - color: rgba(255, 255, 255, 0.55); -} -.navbar-dark .navbar-text a, -.navbar-dark .navbar-text a:hover, -.navbar-dark .navbar-text a:focus { - color: #fff; +[data-bs-theme=dark] .navbar-toggler-icon { + --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e"); } .card { + --bs-card-spacer-y: 1rem; + --bs-card-spacer-x: 1rem; + --bs-card-title-spacer-y: 0.5rem; + --bs-card-title-color: ; + --bs-card-subtitle-color: ; + --bs-card-border-width: var(--bs-border-width); + --bs-card-border-color: var(--bs-border-color-translucent); + --bs-card-border-radius: var(--bs-border-radius); + --bs-card-box-shadow: ; + --bs-card-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); + --bs-card-cap-padding-y: 0.5rem; + --bs-card-cap-padding-x: 1rem; + --bs-card-cap-bg: rgba(var(--bs-body-color-rgb), 0.03); + --bs-card-cap-color: ; + --bs-card-height: ; + --bs-card-color: ; + --bs-card-bg: var(--bs-body-bg); + --bs-card-img-overlay-padding: 1rem; + --bs-card-group-margin: 0.75rem; position: relative; display: flex; flex-direction: column; min-width: 0; + height: var(--bs-card-height); + color: var(--bs-body-color); word-wrap: break-word; - background-color: #fff; + background-color: var(--bs-card-bg); background-clip: border-box; - border: 1px solid rgba(0, 0, 0, 0.125); - border-radius: 0.25rem; + border: var(--bs-card-border-width) solid var(--bs-card-border-color); + border-radius: var(--bs-card-border-radius); } .card > hr { margin-right: 0; @@ -4593,13 +4506,13 @@ textarea.form-control-lg { } .card > .list-group:first-child { border-top-width: 0; - border-top-left-radius: calc(0.25rem - 1px); - border-top-right-radius: calc(0.25rem - 1px); + border-top-left-radius: var(--bs-card-inner-border-radius); + border-top-right-radius: var(--bs-card-inner-border-radius); } .card > .list-group:last-child { border-bottom-width: 0; - border-bottom-right-radius: calc(0.25rem - 1px); - border-bottom-left-radius: calc(0.25rem - 1px); + border-bottom-right-radius: var(--bs-card-inner-border-radius); + border-bottom-left-radius: var(--bs-card-inner-border-radius); } .card > .card-header + .list-group, .card > .list-group + .card-footer { @@ -4608,16 +4521,19 @@ textarea.form-control-lg { .card-body { flex: 1 1 auto; - padding: 1rem 1rem; + padding: var(--bs-card-spacer-y) var(--bs-card-spacer-x); + color: var(--bs-card-color); } .card-title { - margin-bottom: 0.5rem; + margin-bottom: var(--bs-card-title-spacer-y); + color: var(--bs-card-title-color); } .card-subtitle { - margin-top: -0.25rem; + margin-top: calc(-0.5 * var(--bs-card-title-spacer-y)); margin-bottom: 0; + color: var(--bs-card-subtitle-color); } .card-text:last-child { @@ -4625,38 +4541,44 @@ textarea.form-control-lg { } .card-link + .card-link { - margin-left: 1rem; + margin-left: var(--bs-card-spacer-x); } .card-header { - padding: 0.5rem 1rem; + padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); margin-bottom: 0; - background-color: rgba(0, 0, 0, 0.03); - border-bottom: 1px solid rgba(0, 0, 0, 0.125); + color: var(--bs-card-cap-color); + background-color: var(--bs-card-cap-bg); + border-bottom: var(--bs-card-border-width) solid var(--bs-card-border-color); } .card-header:first-child { - border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; + border-radius: var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0; } .card-footer { - padding: 0.5rem 1rem; - background-color: rgba(0, 0, 0, 0.03); - border-top: 1px solid rgba(0, 0, 0, 0.125); + padding: var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x); + color: var(--bs-card-cap-color); + background-color: var(--bs-card-cap-bg); + border-top: var(--bs-card-border-width) solid var(--bs-card-border-color); } .card-footer:last-child { - border-radius: 0 0 calc(0.25rem - 1px) calc(0.25rem - 1px); + border-radius: 0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius); } .card-header-tabs { - margin-right: -0.5rem; - margin-bottom: -0.5rem; - margin-left: -0.5rem; + margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); + margin-bottom: calc(-1 * var(--bs-card-cap-padding-y)); + margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); border-bottom: 0; } +.card-header-tabs .nav-link.active { + background-color: var(--bs-card-bg); + border-bottom-color: var(--bs-card-bg); +} .card-header-pills { - margin-right: -0.5rem; - margin-left: -0.5rem; + margin-right: calc(-0.5 * var(--bs-card-cap-padding-x)); + margin-left: calc(-0.5 * var(--bs-card-cap-padding-x)); } .card-img-overlay { @@ -4665,8 +4587,8 @@ textarea.form-control-lg { right: 0; bottom: 0; left: 0; - padding: 1rem; - border-radius: calc(0.25rem - 1px); + padding: var(--bs-card-img-overlay-padding); + border-radius: var(--bs-card-inner-border-radius); } .card-img, @@ -4677,18 +4599,18 @@ textarea.form-control-lg { .card-img, .card-img-top { - border-top-left-radius: calc(0.25rem - 1px); - border-top-right-radius: calc(0.25rem - 1px); + border-top-left-radius: var(--bs-card-inner-border-radius); + border-top-right-radius: var(--bs-card-inner-border-radius); } .card-img, .card-img-bottom { - border-bottom-right-radius: calc(0.25rem - 1px); - border-bottom-left-radius: calc(0.25rem - 1px); + border-bottom-right-radius: var(--bs-card-inner-border-radius); + border-bottom-left-radius: var(--bs-card-inner-border-radius); } .card-group > .card { - margin-bottom: 0.75rem; + margin-bottom: var(--bs-card-group-margin); } @media (min-width: 576px) { .card-group { @@ -4696,7 +4618,7 @@ textarea.form-control-lg { flex-flow: row wrap; } .card-group > .card { - flex: 1 0 0%; + flex: 1 0 0; margin-bottom: 0; } .card-group > .card + .card { @@ -4707,42 +4629,66 @@ textarea.form-control-lg { border-top-right-radius: 0; border-bottom-right-radius: 0; } - .card-group > .card:not(:last-child) .card-img-top, -.card-group > .card:not(:last-child) .card-header { + .card-group > .card:not(:last-child) > .card-img-top, + .card-group > .card:not(:last-child) > .card-header { border-top-right-radius: 0; } - .card-group > .card:not(:last-child) .card-img-bottom, -.card-group > .card:not(:last-child) .card-footer { + .card-group > .card:not(:last-child) > .card-img-bottom, + .card-group > .card:not(:last-child) > .card-footer { border-bottom-right-radius: 0; } .card-group > .card:not(:first-child) { border-top-left-radius: 0; border-bottom-left-radius: 0; } - .card-group > .card:not(:first-child) .card-img-top, -.card-group > .card:not(:first-child) .card-header { + .card-group > .card:not(:first-child) > .card-img-top, + .card-group > .card:not(:first-child) > .card-header { border-top-left-radius: 0; } - .card-group > .card:not(:first-child) .card-img-bottom, -.card-group > .card:not(:first-child) .card-footer { + .card-group > .card:not(:first-child) > .card-img-bottom, + .card-group > .card:not(:first-child) > .card-footer { border-bottom-left-radius: 0; } } +.accordion { + --bs-accordion-color: var(--bs-body-color); + --bs-accordion-bg: var(--bs-body-bg); + --bs-accordion-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; + --bs-accordion-border-color: var(--bs-border-color); + --bs-accordion-border-width: var(--bs-border-width); + --bs-accordion-border-radius: var(--bs-border-radius); + --bs-accordion-inner-border-radius: calc(var(--bs-border-radius) - (var(--bs-border-width))); + --bs-accordion-btn-padding-x: 1.25rem; + --bs-accordion-btn-padding-y: 1rem; + --bs-accordion-btn-color: var(--bs-body-color); + --bs-accordion-btn-bg: var(--bs-accordion-bg); + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23222' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); + --bs-accordion-btn-icon-width: 1.25rem; + --bs-accordion-btn-icon-transform: rotate(-180deg); + --bs-accordion-btn-icon-transition: transform 0.2s ease-in-out; + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%2308384a' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); + --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.25); + --bs-accordion-body-padding-x: 1.25rem; + --bs-accordion-body-padding-y: 1rem; + --bs-accordion-active-color: var(--bs-primary-text-emphasis); + --bs-accordion-active-bg: var(--bs-primary-bg-subtle); +} + .accordion-button { position: relative; display: flex; align-items: center; width: 100%; - padding: 1rem 1.25rem; + padding: var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x); font-size: 1rem; - color: #222; + color: var(--bs-accordion-btn-color); text-align: left; - background-color: #fff; + background-color: var(--bs-accordion-btn-bg); border: 0; border-radius: 0; overflow-anchor: none; - transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, border-radius 0.15s ease; + transition: var(--bs-accordion-transition); } @media (prefers-reduced-motion: reduce) { .accordion-button { @@ -4750,24 +4696,24 @@ textarea.form-control-lg { } } .accordion-button:not(.collapsed) { - color: #137ea7; - background-color: #e8f4f8; - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.125); + color: var(--bs-accordion-active-color); + background-color: var(--bs-accordion-active-bg); + box-shadow: inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color); } .accordion-button:not(.collapsed)::after { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23137ea7'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); - transform: rotate(-180deg); + background-image: var(--bs-accordion-btn-active-icon); + transform: var(--bs-accordion-btn-icon-transform); } .accordion-button::after { flex-shrink: 0; - width: 1.25rem; - height: 1.25rem; + width: var(--bs-accordion-btn-icon-width); + height: var(--bs-accordion-btn-icon-width); margin-left: auto; content: ""; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23222'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + background-image: var(--bs-accordion-btn-icon); background-repeat: no-repeat; - background-size: 1.25rem; - transition: transform 0.2s ease-in-out; + background-size: var(--bs-accordion-btn-icon-width); + transition: var(--bs-accordion-btn-icon-transition); } @media (prefers-reduced-motion: reduce) { .accordion-button::after { @@ -4779,9 +4725,8 @@ textarea.form-control-lg { } .accordion-button:focus { z-index: 3; - border-color: #8ac6dd; outline: 0; - box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.25); + box-shadow: var(--bs-accordion-btn-focus-box-shadow); } .accordion-header { @@ -4789,79 +4734,113 @@ textarea.form-control-lg { } .accordion-item { - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.125); + color: var(--bs-accordion-color); + background-color: var(--bs-accordion-bg); + border: var(--bs-accordion-border-width) solid var(--bs-accordion-border-color); } .accordion-item:first-of-type { - border-top-left-radius: 0.25rem; - border-top-right-radius: 0.25rem; + border-top-left-radius: var(--bs-accordion-border-radius); + border-top-right-radius: var(--bs-accordion-border-radius); } -.accordion-item:first-of-type .accordion-button { - border-top-left-radius: calc(0.25rem - 1px); - border-top-right-radius: calc(0.25rem - 1px); +.accordion-item:first-of-type > .accordion-header .accordion-button { + border-top-left-radius: var(--bs-accordion-inner-border-radius); + border-top-right-radius: var(--bs-accordion-inner-border-radius); } .accordion-item:not(:first-of-type) { border-top: 0; } .accordion-item:last-of-type { - border-bottom-right-radius: 0.25rem; - border-bottom-left-radius: 0.25rem; + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); } -.accordion-item:last-of-type .accordion-button.collapsed { - border-bottom-right-radius: calc(0.25rem - 1px); - border-bottom-left-radius: calc(0.25rem - 1px); +.accordion-item:last-of-type > .accordion-header .accordion-button.collapsed { + border-bottom-right-radius: var(--bs-accordion-inner-border-radius); + border-bottom-left-radius: var(--bs-accordion-inner-border-radius); } -.accordion-item:last-of-type .accordion-collapse { - border-bottom-right-radius: 0.25rem; - border-bottom-left-radius: 0.25rem; +.accordion-item:last-of-type > .accordion-collapse { + border-bottom-right-radius: var(--bs-accordion-border-radius); + border-bottom-left-radius: var(--bs-accordion-border-radius); } .accordion-body { - padding: 1rem 1.25rem; + padding: var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x); } -.accordion-flush .accordion-collapse { - border-width: 0; -} -.accordion-flush .accordion-item { +.accordion-flush > .accordion-item { border-right: 0; border-left: 0; border-radius: 0; } -.accordion-flush .accordion-item:first-child { +.accordion-flush > .accordion-item:first-child { border-top: 0; } -.accordion-flush .accordion-item:last-child { +.accordion-flush > .accordion-item:last-child { border-bottom: 0; } -.accordion-flush .accordion-item .accordion-button { +.accordion-flush > .accordion-item > .accordion-collapse, +.accordion-flush > .accordion-item > .accordion-header .accordion-button, +.accordion-flush > .accordion-item > .accordion-header .accordion-button.collapsed { border-radius: 0; } +[data-bs-theme=dark] .accordion-button::after { + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2373bad6'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e"); + --bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%2373bad6'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e"); +} + .breadcrumb { + --bs-breadcrumb-padding-x: 0.75rem; + --bs-breadcrumb-padding-y: 0.375rem; + --bs-breadcrumb-margin-bottom: 1rem; + --bs-breadcrumb-bg: var(--bs-tertiary-bg); + --bs-breadcrumb-border-radius: 0.25rem; + --bs-breadcrumb-divider-color: var(--bs-secondary-color); + --bs-breadcrumb-item-padding-x: 0.5rem; + --bs-breadcrumb-item-active-color: var(--bs-secondary-color); display: flex; flex-wrap: wrap; - padding: 0.375rem 0.75rem; - margin-bottom: 1rem; + padding: var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x); + margin-bottom: var(--bs-breadcrumb-margin-bottom); + font-size: var(--bs-breadcrumb-font-size); list-style: none; - background-color: #f0f0f0; - border-radius: 0.25rem; + background-color: var(--bs-breadcrumb-bg); + border-radius: var(--bs-breadcrumb-border-radius); } .breadcrumb-item + .breadcrumb-item { - padding-left: 0.5rem; + padding-left: var(--bs-breadcrumb-item-padding-x); } .breadcrumb-item + .breadcrumb-item::before { float: left; - padding-right: 0.5rem; - color: #999; + padding-right: var(--bs-breadcrumb-item-padding-x); + color: var(--bs-breadcrumb-divider-color); content: var(--bs-breadcrumb-divider, "/") /* rtl: var(--bs-breadcrumb-divider, "/") */; } .breadcrumb-item.active { - color: #999; + color: var(--bs-breadcrumb-item-active-color); } .pagination { + --bs-pagination-padding-x: 0.75rem; + --bs-pagination-padding-y: 0.375rem; + --bs-pagination-font-size: 1rem; + --bs-pagination-color: #555; + --bs-pagination-bg: var(--bs-tertiary-bg); + --bs-pagination-border-width: var(--bs-border-width); + --bs-pagination-border-color: var(--bs-border-color); + --bs-pagination-border-radius: var(--bs-border-radius); + --bs-pagination-hover-color: #555; + --bs-pagination-hover-bg: var(--bs-tertiary-bg); + --bs-pagination-hover-border-color: var(--bs-border-color); + --bs-pagination-focus-color: var(--bs-link-hover-color); + --bs-pagination-focus-bg: var(--bs-secondary-bg); + --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.25); + --bs-pagination-active-color: #fff; + --bs-pagination-active-bg: #158cba; + --bs-pagination-active-border-color: #127ba3; + --bs-pagination-disabled-color: #999; + --bs-pagination-disabled-bg: var(--bs-tertiary-bg); + --bs-pagination-disabled-border-color: var(--bs-border-color); display: flex; padding-left: 0; list-style: none; @@ -4870,10 +4849,12 @@ textarea.form-control-lg { .page-link { position: relative; display: block; - color: #555; + padding: var(--bs-pagination-padding-y) var(--bs-pagination-padding-x); + font-size: var(--bs-pagination-font-size); + color: var(--bs-pagination-color); text-decoration: none; - background-color: #f0f0f0; - border: 1px solid #dee2e6; + background-color: var(--bs-pagination-bg); + border: var(--bs-pagination-border-width) solid var(--bs-pagination-border-color); transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; } @media (prefers-reduced-motion: reduce) { @@ -4883,84 +4864,73 @@ textarea.form-control-lg { } .page-link:hover { z-index: 2; - color: #555; - background-color: #f0f0f0; - border-color: #dee2e6; + color: var(--bs-pagination-hover-color); + background-color: var(--bs-pagination-hover-bg); + border-color: var(--bs-pagination-hover-border-color); } .page-link:focus { z-index: 3; - color: #117095; - background-color: #f0f0f0; + color: var(--bs-pagination-focus-color); + background-color: var(--bs-pagination-focus-bg); outline: 0; - box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.25); + box-shadow: var(--bs-pagination-focus-box-shadow); +} +.page-link.active, .active > .page-link { + z-index: 3; + color: var(--bs-pagination-active-color); + background-color: var(--bs-pagination-active-bg); + border-color: var(--bs-pagination-active-border-color); +} +.page-link.disabled, .disabled > .page-link { + color: var(--bs-pagination-disabled-color); + pointer-events: none; + background-color: var(--bs-pagination-disabled-bg); + border-color: var(--bs-pagination-disabled-border-color); } .page-item:not(:first-child) .page-link { - margin-left: -1px; + margin-left: calc(-1 * var(--bs-border-width)); } -.page-item.active .page-link { - z-index: 3; - color: #fff; - background-color: #158cba; - border-color: #127ba3; -} -.page-item.disabled .page-link { - color: #999; - pointer-events: none; - background-color: #f0f0f0; - border-color: #dee2e6; -} - -.page-link { - padding: 0.375rem 0.75rem; -} - .page-item:first-child .page-link { - border-top-left-radius: 0.25rem; - border-bottom-left-radius: 0.25rem; + border-top-left-radius: var(--bs-pagination-border-radius); + border-bottom-left-radius: var(--bs-pagination-border-radius); } .page-item:last-child .page-link { - border-top-right-radius: 0.25rem; - border-bottom-right-radius: 0.25rem; + border-top-right-radius: var(--bs-pagination-border-radius); + border-bottom-right-radius: var(--bs-pagination-border-radius); } -.pagination-lg .page-link { - padding: 0.75rem 1.5rem; - font-size: 1.25rem; -} -.pagination-lg .page-item:first-child .page-link { - border-top-left-radius: 0.3rem; - border-bottom-left-radius: 0.3rem; -} -.pagination-lg .page-item:last-child .page-link { - border-top-right-radius: 0.3rem; - border-bottom-right-radius: 0.3rem; +.pagination-lg { + --bs-pagination-padding-x: 1.5rem; + --bs-pagination-padding-y: 0.75rem; + --bs-pagination-font-size: 1.25rem; + --bs-pagination-border-radius: var(--bs-border-radius-lg); } -.pagination-sm .page-link { - padding: 0.25rem 0.5rem; - font-size: 0.875rem; -} -.pagination-sm .page-item:first-child .page-link { - border-top-left-radius: 0.2rem; - border-bottom-left-radius: 0.2rem; -} -.pagination-sm .page-item:last-child .page-link { - border-top-right-radius: 0.2rem; - border-bottom-right-radius: 0.2rem; +.pagination-sm { + --bs-pagination-padding-x: 0.5rem; + --bs-pagination-padding-y: 0.25rem; + --bs-pagination-font-size: 0.875rem; + --bs-pagination-border-radius: var(--bs-border-radius-sm); } .badge { + --bs-badge-padding-x: 0.65em; + --bs-badge-padding-y: 0.35em; + --bs-badge-font-size: 0.75em; + --bs-badge-font-weight: 700; + --bs-badge-color: #fff; + --bs-badge-border-radius: var(--bs-border-radius); display: inline-block; - padding: 0.35em 0.65em; - font-size: 0.75em; - font-weight: 700; + padding: var(--bs-badge-padding-y) var(--bs-badge-padding-x); + font-size: var(--bs-badge-font-size); + font-weight: var(--bs-badge-font-weight); line-height: 1; - color: #fff; + color: var(--bs-badge-color); text-align: center; white-space: nowrap; vertical-align: baseline; - border-radius: 0.25rem; + border-radius: var(--bs-badge-border-radius); } .badge:empty { display: none; @@ -4972,11 +4942,22 @@ textarea.form-control-lg { } .alert { + --bs-alert-bg: transparent; + --bs-alert-padding-x: 1rem; + --bs-alert-padding-y: 1rem; + --bs-alert-margin-bottom: 1rem; + --bs-alert-color: inherit; + --bs-alert-border-color: transparent; + --bs-alert-border: var(--bs-border-width) solid var(--bs-alert-border-color); + --bs-alert-border-radius: var(--bs-border-radius); + --bs-alert-link-color: inherit; position: relative; - padding: 1rem 1rem; - margin-bottom: 1rem; - border: 1px solid transparent; - border-radius: 0.25rem; + padding: var(--bs-alert-padding-y) var(--bs-alert-padding-x); + margin-bottom: var(--bs-alert-margin-bottom); + color: var(--bs-alert-color); + background-color: var(--bs-alert-bg); + border: var(--bs-alert-border); + border-radius: var(--bs-alert-border-radius); } .alert-heading { @@ -4985,6 +4966,7 @@ textarea.form-control-lg { .alert-link { font-weight: 700; + color: var(--bs-alert-link-color); } .alert-dismissible { @@ -4999,95 +4981,82 @@ textarea.form-control-lg { } .alert-primary { - color: #0d5470; - background-color: #d0e8f1; - border-color: #b9ddea; -} -.alert-primary .alert-link { - color: #0a435a; + --bs-alert-color: var(--bs-primary-text-emphasis); + --bs-alert-bg: var(--bs-primary-bg-subtle); + --bs-alert-border-color: var(--bs-primary-border-subtle); + --bs-alert-link-color: var(--bs-primary-text-emphasis); } .alert-secondary { - color: #909090; - background-color: #fcfcfc; - border-color: #fbfbfb; -} -.alert-secondary .alert-link { - color: #737373; + --bs-alert-color: var(--bs-secondary-text-emphasis); + --bs-alert-bg: var(--bs-secondary-bg-subtle); + --bs-alert-border-color: var(--bs-secondary-border-subtle); + --bs-alert-link-color: var(--bs-secondary-text-emphasis); } .alert-success { - color: #186d1a; - background-color: #d4f0d5; - border-color: #bfe9c0; -} -.alert-success .alert-link { - color: #135715; + --bs-alert-color: var(--bs-success-text-emphasis); + --bs-alert-bg: var(--bs-success-bg-subtle); + --bs-alert-border-color: var(--bs-success-border-subtle); + --bs-alert-link-color: var(--bs-success-text-emphasis); } .alert-info { - color: #46798d; - background-color: #e3f4fb; - border-color: #d6eff9; -} -.alert-info .alert-link { - color: #386171; + --bs-alert-color: var(--bs-info-text-emphasis); + --bs-alert-bg: var(--bs-info-bg-subtle); + --bs-alert-border-color: var(--bs-info-border-subtle); + --bs-alert-link-color: var(--bs-info-text-emphasis); } .alert-warning { - color: #995010; - background-color: #ffe7d1; - border-color: #ffdabb; -} -.alert-warning .alert-link { - color: #7a400d; + --bs-alert-color: var(--bs-warning-text-emphasis); + --bs-alert-bg: var(--bs-warning-bg-subtle); + --bs-alert-border-color: var(--bs-warning-border-subtle); + --bs-alert-link-color: var(--bs-warning-text-emphasis); } .alert-danger { - color: #992720; - background-color: #ffd9d7; - border-color: #ffc6c3; -} -.alert-danger .alert-link { - color: #7a1f1a; + --bs-alert-color: var(--bs-danger-text-emphasis); + --bs-alert-bg: var(--bs-danger-bg-subtle); + --bs-alert-border-color: var(--bs-danger-border-subtle); + --bs-alert-link-color: var(--bs-danger-text-emphasis); } .alert-light { - color: #949494; - background-color: #fdfdfd; - border-color: #fcfcfc; -} -.alert-light .alert-link { - color: #767676; + --bs-alert-color: var(--bs-light-text-emphasis); + --bs-alert-bg: var(--bs-light-bg-subtle); + --bs-alert-border-color: var(--bs-light-border-subtle); + --bs-alert-link-color: var(--bs-light-text-emphasis); } .alert-dark { - color: #333333; - background-color: #dddddd; - border-color: #cccccc; -} -.alert-dark .alert-link { - color: #292929; -} - -@-webkit-keyframes progress-bar-stripes { - 0% { - background-position-x: 1rem; - } + --bs-alert-color: var(--bs-dark-text-emphasis); + --bs-alert-bg: var(--bs-dark-bg-subtle); + --bs-alert-border-color: var(--bs-dark-border-subtle); + --bs-alert-link-color: var(--bs-dark-text-emphasis); } @keyframes progress-bar-stripes { 0% { - background-position-x: 1rem; + background-position-x: var(--bs-progress-height); } } -.progress { +.progress, +.progress-stacked { + --bs-progress-height: 1rem; + --bs-progress-font-size: 0.75rem; + --bs-progress-bg: var(--bs-secondary-bg); + --bs-progress-border-radius: var(--bs-border-radius); + --bs-progress-box-shadow: var(--bs-box-shadow-inset); + --bs-progress-bar-color: #fff; + --bs-progress-bar-bg: #158cba; + --bs-progress-bar-transition: width 0.6s ease; display: flex; - height: 1rem; + height: var(--bs-progress-height); overflow: hidden; - font-size: 0.75rem; - background-color: #f0f0f0; - border-radius: 0.25rem; + font-size: var(--bs-progress-font-size); + background-color: var(--bs-progress-bg); + border-radius: var(--bs-progress-border-radius); } .progress-bar { @@ -5095,11 +5064,11 @@ textarea.form-control-lg { flex-direction: column; justify-content: center; overflow: hidden; - color: #fff; + color: var(--bs-progress-bar-color); text-align: center; white-space: nowrap; - background-color: #158cba; - transition: width 0.6s ease; + background-color: var(--bs-progress-bar-bg); + transition: var(--bs-progress-bar-transition); } @media (prefers-reduced-motion: reduce) { .progress-bar { @@ -5109,61 +5078,68 @@ textarea.form-control-lg { .progress-bar-striped { background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-size: 1rem 1rem; + background-size: var(--bs-progress-height) var(--bs-progress-height); +} + +.progress-stacked > .progress { + overflow: visible; +} + +.progress-stacked > .progress > .progress-bar { + width: 100%; } .progress-bar-animated { - -webkit-animation: 1s linear infinite progress-bar-stripes; animation: 1s linear infinite progress-bar-stripes; } @media (prefers-reduced-motion: reduce) { .progress-bar-animated { - -webkit-animation: none; animation: none; } } .list-group { + --bs-list-group-color: var(--bs-body-color); + --bs-list-group-bg: var(--bs-body-bg); + --bs-list-group-border-color: var(--bs-border-color); + --bs-list-group-border-width: var(--bs-border-width); + --bs-list-group-border-radius: var(--bs-border-radius); + --bs-list-group-item-padding-x: 1rem; + --bs-list-group-item-padding-y: 0.5rem; + --bs-list-group-action-color: var(--bs-secondary-color); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-tertiary-bg); + --bs-list-group-action-active-color: var(--bs-body-color); + --bs-list-group-action-active-bg: var(--bs-secondary-bg); + --bs-list-group-disabled-color: var(--bs-secondary-color); + --bs-list-group-disabled-bg: var(--bs-body-bg); + --bs-list-group-active-color: #fff; + --bs-list-group-active-bg: #158cba; + --bs-list-group-active-border-color: #158cba; display: flex; flex-direction: column; padding-left: 0; margin-bottom: 0; - border-radius: 0.25rem; + border-radius: var(--bs-list-group-border-radius); } .list-group-numbered { list-style-type: none; counter-reset: section; } -.list-group-numbered > li::before { +.list-group-numbered > .list-group-item::before { content: counters(section, ".") ". "; counter-increment: section; } -.list-group-item-action { - width: 100%; - color: #555; - text-align: inherit; -} -.list-group-item-action:hover, .list-group-item-action:focus { - z-index: 1; - color: #555; - text-decoration: none; - background-color: #f6f6f6; -} -.list-group-item-action:active { - color: #222; - background-color: #f0f0f0; -} - .list-group-item { position: relative; display: block; - padding: 0.5rem 1rem; - color: #222; + padding: var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x); + color: var(--bs-list-group-color); text-decoration: none; - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.125); + background-color: var(--bs-list-group-bg); + border: var(--bs-list-group-border-width) solid var(--bs-list-group-border-color); } .list-group-item:first-child { border-top-left-radius: inherit; @@ -5174,332 +5150,373 @@ textarea.form-control-lg { border-bottom-left-radius: inherit; } .list-group-item.disabled, .list-group-item:disabled { - color: #999; + color: var(--bs-list-group-disabled-color); pointer-events: none; - background-color: #fff; + background-color: var(--bs-list-group-disabled-bg); } .list-group-item.active { z-index: 2; - color: #fff; - background-color: #158cba; - border-color: #158cba; + color: var(--bs-list-group-active-color); + background-color: var(--bs-list-group-active-bg); + border-color: var(--bs-list-group-active-border-color); } .list-group-item + .list-group-item { border-top-width: 0; } .list-group-item + .list-group-item.active { - margin-top: -1px; - border-top-width: 1px; + margin-top: calc(-1 * var(--bs-list-group-border-width)); + border-top-width: var(--bs-list-group-border-width); +} + +.list-group-item-action { + width: 100%; + color: var(--bs-list-group-action-color); + text-align: inherit; +} +.list-group-item-action:not(.active):hover, .list-group-item-action:not(.active):focus { + z-index: 1; + color: var(--bs-list-group-action-hover-color); + text-decoration: none; + background-color: var(--bs-list-group-action-hover-bg); +} +.list-group-item-action:not(.active):active { + color: var(--bs-list-group-action-active-color); + background-color: var(--bs-list-group-action-active-bg); } .list-group-horizontal { flex-direction: row; } -.list-group-horizontal > .list-group-item:first-child { - border-bottom-left-radius: 0.25rem; +.list-group-horizontal > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } -.list-group-horizontal > .list-group-item:last-child { - border-top-right-radius: 0.25rem; +.list-group-horizontal > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } .list-group-horizontal > .list-group-item.active { margin-top: 0; } .list-group-horizontal > .list-group-item + .list-group-item { - border-top-width: 1px; + border-top-width: var(--bs-list-group-border-width); border-left-width: 0; } .list-group-horizontal > .list-group-item + .list-group-item.active { - margin-left: -1px; - border-left-width: 1px; + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); } @media (min-width: 576px) { .list-group-horizontal-sm { flex-direction: row; } - .list-group-horizontal-sm > .list-group-item:first-child { - border-bottom-left-radius: 0.25rem; + .list-group-horizontal-sm > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-sm > .list-group-item:last-child { - border-top-right-radius: 0.25rem; + .list-group-horizontal-sm > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } .list-group-horizontal-sm > .list-group-item.active { margin-top: 0; } .list-group-horizontal-sm > .list-group-item + .list-group-item { - border-top-width: 1px; + border-top-width: var(--bs-list-group-border-width); border-left-width: 0; } .list-group-horizontal-sm > .list-group-item + .list-group-item.active { - margin-left: -1px; - border-left-width: 1px; + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); } } @media (min-width: 768px) { .list-group-horizontal-md { flex-direction: row; } - .list-group-horizontal-md > .list-group-item:first-child { - border-bottom-left-radius: 0.25rem; + .list-group-horizontal-md > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-md > .list-group-item:last-child { - border-top-right-radius: 0.25rem; + .list-group-horizontal-md > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } .list-group-horizontal-md > .list-group-item.active { margin-top: 0; } .list-group-horizontal-md > .list-group-item + .list-group-item { - border-top-width: 1px; + border-top-width: var(--bs-list-group-border-width); border-left-width: 0; } .list-group-horizontal-md > .list-group-item + .list-group-item.active { - margin-left: -1px; - border-left-width: 1px; + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); } } @media (min-width: 992px) { .list-group-horizontal-lg { flex-direction: row; } - .list-group-horizontal-lg > .list-group-item:first-child { - border-bottom-left-radius: 0.25rem; + .list-group-horizontal-lg > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-lg > .list-group-item:last-child { - border-top-right-radius: 0.25rem; + .list-group-horizontal-lg > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } .list-group-horizontal-lg > .list-group-item.active { margin-top: 0; } .list-group-horizontal-lg > .list-group-item + .list-group-item { - border-top-width: 1px; + border-top-width: var(--bs-list-group-border-width); border-left-width: 0; } .list-group-horizontal-lg > .list-group-item + .list-group-item.active { - margin-left: -1px; - border-left-width: 1px; + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); } } @media (min-width: 1200px) { .list-group-horizontal-xl { flex-direction: row; } - .list-group-horizontal-xl > .list-group-item:first-child { - border-bottom-left-radius: 0.25rem; + .list-group-horizontal-xl > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-xl > .list-group-item:last-child { - border-top-right-radius: 0.25rem; + .list-group-horizontal-xl > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } .list-group-horizontal-xl > .list-group-item.active { margin-top: 0; } .list-group-horizontal-xl > .list-group-item + .list-group-item { - border-top-width: 1px; + border-top-width: var(--bs-list-group-border-width); border-left-width: 0; } .list-group-horizontal-xl > .list-group-item + .list-group-item.active { - margin-left: -1px; - border-left-width: 1px; + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); } } @media (min-width: 1400px) { .list-group-horizontal-xxl { flex-direction: row; } - .list-group-horizontal-xxl > .list-group-item:first-child { - border-bottom-left-radius: 0.25rem; + .list-group-horizontal-xxl > .list-group-item:first-child:not(:last-child) { + border-bottom-left-radius: var(--bs-list-group-border-radius); border-top-right-radius: 0; } - .list-group-horizontal-xxl > .list-group-item:last-child { - border-top-right-radius: 0.25rem; + .list-group-horizontal-xxl > .list-group-item:last-child:not(:first-child) { + border-top-right-radius: var(--bs-list-group-border-radius); border-bottom-left-radius: 0; } .list-group-horizontal-xxl > .list-group-item.active { margin-top: 0; } .list-group-horizontal-xxl > .list-group-item + .list-group-item { - border-top-width: 1px; + border-top-width: var(--bs-list-group-border-width); border-left-width: 0; } .list-group-horizontal-xxl > .list-group-item + .list-group-item.active { - margin-left: -1px; - border-left-width: 1px; + margin-left: calc(-1 * var(--bs-list-group-border-width)); + border-left-width: var(--bs-list-group-border-width); } } .list-group-flush { border-radius: 0; } .list-group-flush > .list-group-item { - border-width: 0 0 1px; + border-width: 0 0 var(--bs-list-group-border-width); } .list-group-flush > .list-group-item:last-child { border-bottom-width: 0; } .list-group-item-primary { - color: #0d5470; - background-color: #d0e8f1; -} -.list-group-item-primary.list-group-item-action:hover, .list-group-item-primary.list-group-item-action:focus { - color: #0d5470; - background-color: #bbd1d9; -} -.list-group-item-primary.list-group-item-action.active { - color: #fff; - background-color: #0d5470; - border-color: #0d5470; + --bs-list-group-color: var(--bs-primary-text-emphasis); + --bs-list-group-bg: var(--bs-primary-bg-subtle); + --bs-list-group-border-color: var(--bs-primary-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-primary-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-primary-border-subtle); + --bs-list-group-active-color: var(--bs-primary-bg-subtle); + --bs-list-group-active-bg: var(--bs-primary-text-emphasis); + --bs-list-group-active-border-color: var(--bs-primary-text-emphasis); } .list-group-item-secondary { - color: #909090; - background-color: #fcfcfc; -} -.list-group-item-secondary.list-group-item-action:hover, .list-group-item-secondary.list-group-item-action:focus { - color: #909090; - background-color: #e3e3e3; -} -.list-group-item-secondary.list-group-item-action.active { - color: #fff; - background-color: #909090; - border-color: #909090; + --bs-list-group-color: var(--bs-secondary-text-emphasis); + --bs-list-group-bg: var(--bs-secondary-bg-subtle); + --bs-list-group-border-color: var(--bs-secondary-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-secondary-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-secondary-border-subtle); + --bs-list-group-active-color: var(--bs-secondary-bg-subtle); + --bs-list-group-active-bg: var(--bs-secondary-text-emphasis); + --bs-list-group-active-border-color: var(--bs-secondary-text-emphasis); } .list-group-item-success { - color: #186d1a; - background-color: #d4f0d5; -} -.list-group-item-success.list-group-item-action:hover, .list-group-item-success.list-group-item-action:focus { - color: #186d1a; - background-color: #bfd8c0; -} -.list-group-item-success.list-group-item-action.active { - color: #fff; - background-color: #186d1a; - border-color: #186d1a; + --bs-list-group-color: var(--bs-success-text-emphasis); + --bs-list-group-bg: var(--bs-success-bg-subtle); + --bs-list-group-border-color: var(--bs-success-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-success-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-success-border-subtle); + --bs-list-group-active-color: var(--bs-success-bg-subtle); + --bs-list-group-active-bg: var(--bs-success-text-emphasis); + --bs-list-group-active-border-color: var(--bs-success-text-emphasis); } .list-group-item-info { - color: #46798d; - background-color: #e3f4fb; -} -.list-group-item-info.list-group-item-action:hover, .list-group-item-info.list-group-item-action:focus { - color: #46798d; - background-color: #ccdce2; -} -.list-group-item-info.list-group-item-action.active { - color: #fff; - background-color: #46798d; - border-color: #46798d; + --bs-list-group-color: var(--bs-info-text-emphasis); + --bs-list-group-bg: var(--bs-info-bg-subtle); + --bs-list-group-border-color: var(--bs-info-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-info-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-info-border-subtle); + --bs-list-group-active-color: var(--bs-info-bg-subtle); + --bs-list-group-active-bg: var(--bs-info-text-emphasis); + --bs-list-group-active-border-color: var(--bs-info-text-emphasis); } .list-group-item-warning { - color: #995010; - background-color: #ffe7d1; -} -.list-group-item-warning.list-group-item-action:hover, .list-group-item-warning.list-group-item-action:focus { - color: #995010; - background-color: #e6d0bc; -} -.list-group-item-warning.list-group-item-action.active { - color: #fff; - background-color: #995010; - border-color: #995010; + --bs-list-group-color: var(--bs-warning-text-emphasis); + --bs-list-group-bg: var(--bs-warning-bg-subtle); + --bs-list-group-border-color: var(--bs-warning-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-warning-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-warning-border-subtle); + --bs-list-group-active-color: var(--bs-warning-bg-subtle); + --bs-list-group-active-bg: var(--bs-warning-text-emphasis); + --bs-list-group-active-border-color: var(--bs-warning-text-emphasis); } .list-group-item-danger { - color: #992720; - background-color: #ffd9d7; -} -.list-group-item-danger.list-group-item-action:hover, .list-group-item-danger.list-group-item-action:focus { - color: #992720; - background-color: #e6c3c2; -} -.list-group-item-danger.list-group-item-action.active { - color: #fff; - background-color: #992720; - border-color: #992720; + --bs-list-group-color: var(--bs-danger-text-emphasis); + --bs-list-group-bg: var(--bs-danger-bg-subtle); + --bs-list-group-border-color: var(--bs-danger-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-danger-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-danger-border-subtle); + --bs-list-group-active-color: var(--bs-danger-bg-subtle); + --bs-list-group-active-bg: var(--bs-danger-text-emphasis); + --bs-list-group-active-border-color: var(--bs-danger-text-emphasis); } .list-group-item-light { - color: #949494; - background-color: #fdfdfd; -} -.list-group-item-light.list-group-item-action:hover, .list-group-item-light.list-group-item-action:focus { - color: #949494; - background-color: #e4e4e4; -} -.list-group-item-light.list-group-item-action.active { - color: #fff; - background-color: #949494; - border-color: #949494; + --bs-list-group-color: var(--bs-light-text-emphasis); + --bs-list-group-bg: var(--bs-light-bg-subtle); + --bs-list-group-border-color: var(--bs-light-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-light-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-light-border-subtle); + --bs-list-group-active-color: var(--bs-light-bg-subtle); + --bs-list-group-active-bg: var(--bs-light-text-emphasis); + --bs-list-group-active-border-color: var(--bs-light-text-emphasis); } .list-group-item-dark { - color: #333333; - background-color: #dddddd; -} -.list-group-item-dark.list-group-item-action:hover, .list-group-item-dark.list-group-item-action:focus { - color: #333333; - background-color: #c7c7c7; -} -.list-group-item-dark.list-group-item-action.active { - color: #fff; - background-color: #333333; - border-color: #333333; + --bs-list-group-color: var(--bs-dark-text-emphasis); + --bs-list-group-bg: var(--bs-dark-bg-subtle); + --bs-list-group-border-color: var(--bs-dark-border-subtle); + --bs-list-group-action-hover-color: var(--bs-emphasis-color); + --bs-list-group-action-hover-bg: var(--bs-dark-border-subtle); + --bs-list-group-action-active-color: var(--bs-emphasis-color); + --bs-list-group-action-active-bg: var(--bs-dark-border-subtle); + --bs-list-group-active-color: var(--bs-dark-bg-subtle); + --bs-list-group-active-bg: var(--bs-dark-text-emphasis); + --bs-list-group-active-border-color: var(--bs-dark-text-emphasis); } .btn-close { + --bs-btn-close-color: #fff; + --bs-btn-close-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%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.414'/%3e%3c/svg%3e"); + --bs-btn-close-opacity: 0.4; + --bs-btn-close-hover-opacity: 1; + --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.25); + --bs-btn-close-focus-opacity: 1; + --bs-btn-close-disabled-opacity: 0.25; box-sizing: content-box; width: 1em; height: 1em; padding: 0.25em 0.25em; - color: #fff; - background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat; + color: var(--bs-btn-close-color); + background: transparent var(--bs-btn-close-bg) center/1em auto no-repeat; + filter: var(--bs-btn-close-filter); border: 0; - border-radius: 0.25rem; - opacity: 0.4; + border-radius: 0.375rem; + opacity: var(--bs-btn-close-opacity); } .btn-close:hover { - color: #fff; + color: var(--bs-btn-close-color); text-decoration: none; - opacity: 1; + opacity: var(--bs-btn-close-hover-opacity); } .btn-close:focus { outline: 0; - box-shadow: 0 0 0 0.25rem rgba(21, 140, 186, 0.25); - opacity: 1; + box-shadow: var(--bs-btn-close-focus-shadow); + opacity: var(--bs-btn-close-focus-opacity); } .btn-close:disabled, .btn-close.disabled { pointer-events: none; -webkit-user-select: none; -moz-user-select: none; user-select: none; - opacity: 0.25; + opacity: var(--bs-btn-close-disabled-opacity); } .btn-close-white { - filter: invert(1) grayscale(100%) brightness(200%); + --bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%); +} + +:root, +[data-bs-theme=light] { + --bs-btn-close-filter: ; +} + +[data-bs-theme=dark] { + --bs-btn-close-filter: invert(1) grayscale(100%) brightness(200%); } .toast { - width: 350px; + --bs-toast-zindex: 1090; + --bs-toast-padding-x: 0.75rem; + --bs-toast-padding-y: 0.5rem; + --bs-toast-spacing: 1.5rem; + --bs-toast-max-width: 350px; + --bs-toast-font-size: 0.875rem; + --bs-toast-color: ; + --bs-toast-bg: rgba(var(--bs-body-bg-rgb), 0.85); + --bs-toast-border-width: var(--bs-border-width); + --bs-toast-border-color: var(--bs-border-color-translucent); + --bs-toast-border-radius: var(--bs-border-radius); + --bs-toast-box-shadow: var(--bs-box-shadow); + --bs-toast-header-color: var(--bs-secondary-color); + --bs-toast-header-bg: rgba(var(--bs-body-bg-rgb), 0.85); + --bs-toast-header-border-color: var(--bs-border-color-translucent); + width: var(--bs-toast-max-width); max-width: 100%; - font-size: 0.875rem; + font-size: var(--bs-toast-font-size); + color: var(--bs-toast-color); pointer-events: auto; - background-color: rgba(255, 255, 255, 0.85); + background-color: var(--bs-toast-bg); background-clip: padding-box; - border: 1px solid rgba(0, 0, 0, 0.1); - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); - border-radius: 0.25rem; + border: var(--bs-toast-border-width) solid var(--bs-toast-border-color); + box-shadow: var(--bs-toast-box-shadow); + border-radius: var(--bs-toast-border-radius); } .toast.showing { opacity: 0; @@ -5509,6 +5526,9 @@ textarea.form-control-lg { } .toast-container { + --bs-toast-zindex: 1090; + position: absolute; + z-index: var(--bs-toast-zindex); width: -webkit-max-content; width: -moz-max-content; width: max-content; @@ -5516,35 +5536,56 @@ textarea.form-control-lg { pointer-events: none; } .toast-container > :not(:last-child) { - margin-bottom: 0.75rem; + margin-bottom: var(--bs-toast-spacing); } .toast-header { display: flex; align-items: center; - padding: 0.5rem 0.75rem; - color: #999; - background-color: rgba(255, 255, 255, 0.85); + padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x); + color: var(--bs-toast-header-color); + background-color: var(--bs-toast-header-bg); background-clip: padding-box; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - border-top-left-radius: calc(0.25rem - 1px); - border-top-right-radius: calc(0.25rem - 1px); + border-bottom: var(--bs-toast-border-width) solid var(--bs-toast-header-border-color); + border-top-left-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); + border-top-right-radius: calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width)); } .toast-header .btn-close { - margin-right: -0.375rem; - margin-left: 0.75rem; + margin-right: calc(-0.5 * var(--bs-toast-padding-x)); + margin-left: var(--bs-toast-padding-x); } .toast-body { - padding: 0.75rem; + padding: var(--bs-toast-padding-x); word-wrap: break-word; } .modal { + --bs-modal-zindex: 1055; + --bs-modal-width: 500px; + --bs-modal-padding: 1rem; + --bs-modal-margin: 0.5rem; + --bs-modal-color: var(--bs-body-color); + --bs-modal-bg: var(--bs-body-bg); + --bs-modal-border-color: rgba(0, 0, 0, 0.1); + --bs-modal-border-width: var(--bs-border-width); + --bs-modal-border-radius: var(--bs-border-radius-lg); + --bs-modal-box-shadow: var(--bs-box-shadow-sm); + --bs-modal-inner-border-radius: calc(var(--bs-border-radius-lg) - (var(--bs-border-width))); + --bs-modal-header-padding-x: 1rem; + --bs-modal-header-padding-y: 1rem; + --bs-modal-header-padding: 1rem 1rem; + --bs-modal-header-border-color: var(--bs-border-color); + --bs-modal-header-border-width: var(--bs-border-width); + --bs-modal-title-line-height: 1.5; + --bs-modal-footer-gap: 0.5rem; + --bs-modal-footer-bg: ; + --bs-modal-footer-border-color: var(--bs-border-color); + --bs-modal-footer-border-width: var(--bs-border-width); position: fixed; top: 0; left: 0; - z-index: 1055; + z-index: var(--bs-modal-zindex); display: none; width: 100%; height: 100%; @@ -5556,12 +5597,12 @@ textarea.form-control-lg { .modal-dialog { position: relative; width: auto; - margin: 0.5rem; + margin: var(--bs-modal-margin); pointer-events: none; } .modal.fade .modal-dialog { - transition: transform 0.3s ease-out; transform: translate(0, -50px); + transition: transform 0.3s ease-out; } @media (prefers-reduced-motion: reduce) { .modal.fade .modal-dialog { @@ -5576,7 +5617,7 @@ textarea.form-control-lg { } .modal-dialog-scrollable { - height: calc(100% - 1rem); + height: calc(100% - var(--bs-modal-margin) * 2); } .modal-dialog-scrollable .modal-content { max-height: 100%; @@ -5589,7 +5630,7 @@ textarea.form-control-lg { .modal-dialog-centered { display: flex; align-items: center; - min-height: calc(100% - 1rem); + min-height: calc(100% - var(--bs-modal-margin) * 2); } .modal-content { @@ -5597,98 +5638,101 @@ textarea.form-control-lg { display: flex; flex-direction: column; width: 100%; + color: var(--bs-modal-color); pointer-events: auto; - background-color: #fff; + background-color: var(--bs-modal-bg); background-clip: padding-box; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 0.3rem; + border: var(--bs-modal-border-width) solid var(--bs-modal-border-color); + border-radius: var(--bs-modal-border-radius); outline: 0; } .modal-backdrop { + --bs-backdrop-zindex: 1050; + --bs-backdrop-bg: #000; + --bs-backdrop-opacity: 0.5; position: fixed; top: 0; left: 0; - z-index: 1050; + z-index: var(--bs-backdrop-zindex); width: 100vw; height: 100vh; - background-color: #000; + background-color: var(--bs-backdrop-bg); } .modal-backdrop.fade { opacity: 0; } .modal-backdrop.show { - opacity: 0.5; + opacity: var(--bs-backdrop-opacity); } .modal-header { display: flex; flex-shrink: 0; align-items: center; - justify-content: space-between; - padding: 1rem 1rem; - border-bottom: 1px solid #dee2e6; - border-top-left-radius: calc(0.3rem - 1px); - border-top-right-radius: calc(0.3rem - 1px); + padding: var(--bs-modal-header-padding); + border-bottom: var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color); + border-top-left-radius: var(--bs-modal-inner-border-radius); + border-top-right-radius: var(--bs-modal-inner-border-radius); } .modal-header .btn-close { - padding: 0.5rem 0.5rem; - margin: -0.5rem -0.5rem -0.5rem auto; + padding: calc(var(--bs-modal-header-padding-y) * 0.5) calc(var(--bs-modal-header-padding-x) * 0.5); + margin-top: calc(-0.5 * var(--bs-modal-header-padding-y)); + margin-right: calc(-0.5 * var(--bs-modal-header-padding-x)); + margin-bottom: calc(-0.5 * var(--bs-modal-header-padding-y)); + margin-left: auto; } .modal-title { margin-bottom: 0; - line-height: 1.5; + line-height: var(--bs-modal-title-line-height); } .modal-body { position: relative; flex: 1 1 auto; - padding: 1rem; + padding: var(--bs-modal-padding); } .modal-footer { display: flex; - flex-wrap: wrap; flex-shrink: 0; + flex-wrap: wrap; align-items: center; justify-content: flex-end; - padding: 0.75rem; - border-top: 1px solid #dee2e6; - border-bottom-right-radius: calc(0.3rem - 1px); - border-bottom-left-radius: calc(0.3rem - 1px); + padding: calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * 0.5); + background-color: var(--bs-modal-footer-bg); + border-top: var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color); + border-bottom-right-radius: var(--bs-modal-inner-border-radius); + border-bottom-left-radius: var(--bs-modal-inner-border-radius); } .modal-footer > * { - margin: 0.25rem; + margin: calc(var(--bs-modal-footer-gap) * 0.5); } @media (min-width: 576px) { + .modal { + --bs-modal-margin: 1.75rem; + --bs-modal-box-shadow: var(--bs-box-shadow); + } .modal-dialog { - max-width: 500px; - margin: 1.75rem auto; + max-width: var(--bs-modal-width); + margin-right: auto; + margin-left: auto; } - - .modal-dialog-scrollable { - height: calc(100% - 3.5rem); - } - - .modal-dialog-centered { - min-height: calc(100% - 3.5rem); - } - .modal-sm { - max-width: 300px; + --bs-modal-width: 300px; } } @media (min-width: 992px) { .modal-lg, -.modal-xl { - max-width: 800px; + .modal-xl { + --bs-modal-width: 800px; } } @media (min-width: 1200px) { .modal-xl { - max-width: 1140px; + --bs-modal-width: 1140px; } } .modal-fullscreen { @@ -5702,15 +5746,13 @@ textarea.form-control-lg { border: 0; border-radius: 0; } -.modal-fullscreen .modal-header { +.modal-fullscreen .modal-header, +.modal-fullscreen .modal-footer { border-radius: 0; } .modal-fullscreen .modal-body { overflow-y: auto; } -.modal-fullscreen .modal-footer { - border-radius: 0; -} @media (max-width: 575.98px) { .modal-fullscreen-sm-down { @@ -5724,15 +5766,13 @@ textarea.form-control-lg { border: 0; border-radius: 0; } - .modal-fullscreen-sm-down .modal-header { + .modal-fullscreen-sm-down .modal-header, + .modal-fullscreen-sm-down .modal-footer { border-radius: 0; } .modal-fullscreen-sm-down .modal-body { overflow-y: auto; } - .modal-fullscreen-sm-down .modal-footer { - border-radius: 0; - } } @media (max-width: 767.98px) { .modal-fullscreen-md-down { @@ -5746,15 +5786,13 @@ textarea.form-control-lg { border: 0; border-radius: 0; } - .modal-fullscreen-md-down .modal-header { + .modal-fullscreen-md-down .modal-header, + .modal-fullscreen-md-down .modal-footer { border-radius: 0; } .modal-fullscreen-md-down .modal-body { overflow-y: auto; } - .modal-fullscreen-md-down .modal-footer { - border-radius: 0; - } } @media (max-width: 991.98px) { .modal-fullscreen-lg-down { @@ -5768,15 +5806,13 @@ textarea.form-control-lg { border: 0; border-radius: 0; } - .modal-fullscreen-lg-down .modal-header { + .modal-fullscreen-lg-down .modal-header, + .modal-fullscreen-lg-down .modal-footer { border-radius: 0; } .modal-fullscreen-lg-down .modal-body { overflow-y: auto; } - .modal-fullscreen-lg-down .modal-footer { - border-radius: 0; - } } @media (max-width: 1199.98px) { .modal-fullscreen-xl-down { @@ -5790,15 +5826,13 @@ textarea.form-control-lg { border: 0; border-radius: 0; } - .modal-fullscreen-xl-down .modal-header { + .modal-fullscreen-xl-down .modal-header, + .modal-fullscreen-xl-down .modal-footer { border-radius: 0; } .modal-fullscreen-xl-down .modal-body { overflow-y: auto; } - .modal-fullscreen-xl-down .modal-footer { - border-radius: 0; - } } @media (max-width: 1399.98px) { .modal-fullscreen-xxl-down { @@ -5812,21 +5846,30 @@ textarea.form-control-lg { border: 0; border-radius: 0; } - .modal-fullscreen-xxl-down .modal-header { + .modal-fullscreen-xxl-down .modal-header, + .modal-fullscreen-xxl-down .modal-footer { border-radius: 0; } .modal-fullscreen-xxl-down .modal-body { overflow-y: auto; } - .modal-fullscreen-xxl-down .modal-footer { - border-radius: 0; - } } .tooltip { - position: absolute; - z-index: 1080; + --bs-tooltip-zindex: 1080; + --bs-tooltip-max-width: 200px; + --bs-tooltip-padding-x: 0.5rem; + --bs-tooltip-padding-y: 0.25rem; + --bs-tooltip-margin: ; + --bs-tooltip-font-size: 0.875rem; + --bs-tooltip-color: var(--bs-body-bg); + --bs-tooltip-bg: var(--bs-emphasis-color); + --bs-tooltip-border-radius: var(--bs-border-radius); + --bs-tooltip-opacity: 0.9; + --bs-tooltip-arrow-width: 0.8rem; + --bs-tooltip-arrow-height: 0.4rem; + z-index: var(--bs-tooltip-zindex); display: block; - margin: 0; + margin: var(--bs-tooltip-margin); font-family: var(--bs-font-sans-serif); font-style: normal; font-weight: 400; @@ -5838,21 +5881,20 @@ textarea.form-control-lg { text-transform: none; letter-spacing: normal; word-break: normal; - word-spacing: normal; white-space: normal; + word-spacing: normal; line-break: auto; - font-size: 0.875rem; + font-size: var(--bs-tooltip-font-size); word-wrap: break-word; opacity: 0; } .tooltip.show { - opacity: 0.9; + opacity: var(--bs-tooltip-opacity); } .tooltip .tooltip-arrow { - position: absolute; display: block; - width: 0.8rem; - height: 0.4rem; + width: var(--bs-tooltip-arrow-width); + height: var(--bs-tooltip-arrow-height); } .tooltip .tooltip-arrow::before { position: absolute; @@ -5861,74 +5903,83 @@ textarea.form-control-lg { border-style: solid; } -.bs-tooltip-top, .bs-tooltip-auto[data-popper-placement^=top] { - padding: 0.4rem 0; -} .bs-tooltip-top .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow { - bottom: 0; + bottom: calc(-1 * var(--bs-tooltip-arrow-height)); } .bs-tooltip-top .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before { top: -1px; - border-width: 0.4rem 0.4rem 0; - border-top-color: #000; + border-width: var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; + border-top-color: var(--bs-tooltip-bg); } -.bs-tooltip-end, .bs-tooltip-auto[data-popper-placement^=right] { - padding: 0 0.4rem; -} +/* rtl:begin:ignore */ .bs-tooltip-end .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow { - left: 0; - width: 0.4rem; - height: 0.8rem; + left: calc(-1 * var(--bs-tooltip-arrow-height)); + width: var(--bs-tooltip-arrow-height); + height: var(--bs-tooltip-arrow-width); } .bs-tooltip-end .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before { right: -1px; - border-width: 0.4rem 0.4rem 0.4rem 0; - border-right-color: #000; + border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * 0.5) 0; + border-right-color: var(--bs-tooltip-bg); } -.bs-tooltip-bottom, .bs-tooltip-auto[data-popper-placement^=bottom] { - padding: 0.4rem 0; -} +/* rtl:end:ignore */ .bs-tooltip-bottom .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow { - top: 0; + top: calc(-1 * var(--bs-tooltip-arrow-height)); } .bs-tooltip-bottom .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before { bottom: -1px; - border-width: 0 0.4rem 0.4rem; - border-bottom-color: #000; + border-width: 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); + border-bottom-color: var(--bs-tooltip-bg); } -.bs-tooltip-start, .bs-tooltip-auto[data-popper-placement^=left] { - padding: 0 0.4rem; -} +/* rtl:begin:ignore */ .bs-tooltip-start .tooltip-arrow, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow { - right: 0; - width: 0.4rem; - height: 0.8rem; + right: calc(-1 * var(--bs-tooltip-arrow-height)); + width: var(--bs-tooltip-arrow-height); + height: var(--bs-tooltip-arrow-width); } .bs-tooltip-start .tooltip-arrow::before, .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before { left: -1px; - border-width: 0.4rem 0 0.4rem 0.4rem; - border-left-color: #000; + border-width: calc(var(--bs-tooltip-arrow-width) * 0.5) 0 calc(var(--bs-tooltip-arrow-width) * 0.5) var(--bs-tooltip-arrow-height); + border-left-color: var(--bs-tooltip-bg); } +/* rtl:end:ignore */ .tooltip-inner { - max-width: 200px; - padding: 0.25rem 0.5rem; - color: #fff; + max-width: var(--bs-tooltip-max-width); + padding: var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x); + color: var(--bs-tooltip-color); text-align: center; - background-color: #000; - border-radius: 0.25rem; + background-color: var(--bs-tooltip-bg); + border-radius: var(--bs-tooltip-border-radius); } .popover { - position: absolute; - top: 0; - left: 0 /* rtl:ignore */; - z-index: 1070; + --bs-popover-zindex: 1070; + --bs-popover-max-width: 276px; + --bs-popover-font-size: 0.875rem; + --bs-popover-bg: var(--bs-body-bg); + --bs-popover-border-width: var(--bs-border-width); + --bs-popover-border-color: var(--bs-border-color-translucent); + --bs-popover-border-radius: var(--bs-border-radius-lg); + --bs-popover-inner-border-radius: calc(var(--bs-border-radius-lg) - var(--bs-border-width)); + --bs-popover-box-shadow: var(--bs-box-shadow); + --bs-popover-header-padding-x: 1rem; + --bs-popover-header-padding-y: 0.5rem; + --bs-popover-header-font-size: 1rem; + --bs-popover-header-color: inherit; + --bs-popover-header-bg: var(--bs-secondary-bg); + --bs-popover-body-padding-x: 1rem; + --bs-popover-body-padding-y: 1rem; + --bs-popover-body-color: var(--bs-body-color); + --bs-popover-arrow-width: 1rem; + --bs-popover-arrow-height: 0.5rem; + --bs-popover-arrow-border: var(--bs-popover-border-color); + z-index: var(--bs-popover-zindex); display: block; - max-width: 276px; + max-width: var(--bs-popover-max-width); font-family: var(--bs-font-sans-serif); font-style: normal; font-weight: 400; @@ -5940,21 +5991,20 @@ textarea.form-control-lg { text-transform: none; letter-spacing: normal; word-break: normal; - word-spacing: normal; white-space: normal; + word-spacing: normal; line-break: auto; - font-size: 0.875rem; + font-size: var(--bs-popover-font-size); word-wrap: break-word; - background-color: #fff; + background-color: var(--bs-popover-bg); background-clip: padding-box; - border: 1px solid rgba(0, 0, 0, 0.2); - border-radius: 0.3rem; + border: var(--bs-popover-border-width) solid var(--bs-popover-border-color); + border-radius: var(--bs-popover-border-radius); } .popover .popover-arrow { - position: absolute; display: block; - width: 1rem; - height: 0.5rem; + width: var(--bs-popover-arrow-width); + height: var(--bs-popover-arrow-height); } .popover .popover-arrow::before, .popover .popover-arrow::after { position: absolute; @@ -5962,94 +6012,104 @@ textarea.form-control-lg { content: ""; border-color: transparent; border-style: solid; + border-width: 0; } .bs-popover-top > .popover-arrow, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow { - bottom: calc(-0.5rem - 1px); + bottom: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); +} +.bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before, .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { + border-width: var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; } .bs-popover-top > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::before { bottom: 0; - border-width: 0.5rem 0.5rem 0; - border-top-color: rgba(0, 0, 0, 0.25); + border-top-color: var(--bs-popover-arrow-border); } .bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=top] > .popover-arrow::after { - bottom: 1px; - border-width: 0.5rem 0.5rem 0; - border-top-color: #fff; + bottom: var(--bs-popover-border-width); + border-top-color: var(--bs-popover-bg); } +/* rtl:begin:ignore */ .bs-popover-end > .popover-arrow, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow { - left: calc(-0.5rem - 1px); - width: 0.5rem; - height: 1rem; + left: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); + width: var(--bs-popover-arrow-height); + height: var(--bs-popover-arrow-width); +} +.bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before, .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { + border-width: calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * 0.5) 0; } .bs-popover-end > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::before { left: 0; - border-width: 0.5rem 0.5rem 0.5rem 0; - border-right-color: rgba(0, 0, 0, 0.25); + border-right-color: var(--bs-popover-arrow-border); } .bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=right] > .popover-arrow::after { - left: 1px; - border-width: 0.5rem 0.5rem 0.5rem 0; - border-right-color: #fff; + left: var(--bs-popover-border-width); + border-right-color: var(--bs-popover-bg); } +/* rtl:end:ignore */ .bs-popover-bottom > .popover-arrow, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow { - top: calc(-0.5rem - 1px); + top: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); +} +.bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before, .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { + border-width: 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); } .bs-popover-bottom > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::before { top: 0; - border-width: 0 0.5rem 0.5rem 0.5rem; - border-bottom-color: rgba(0, 0, 0, 0.25); + border-bottom-color: var(--bs-popover-arrow-border); } .bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=bottom] > .popover-arrow::after { - top: 1px; - border-width: 0 0.5rem 0.5rem 0.5rem; - border-bottom-color: #fff; + top: var(--bs-popover-border-width); + border-bottom-color: var(--bs-popover-bg); } .bs-popover-bottom .popover-header::before, .bs-popover-auto[data-popper-placement^=bottom] .popover-header::before { position: absolute; top: 0; left: 50%; display: block; - width: 1rem; - margin-left: -0.5rem; + width: var(--bs-popover-arrow-width); + margin-left: calc(-0.5 * var(--bs-popover-arrow-width)); content: ""; - border-bottom: 1px solid #f0f0f0; + border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-header-bg); } +/* rtl:begin:ignore */ .bs-popover-start > .popover-arrow, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow { - right: calc(-0.5rem - 1px); - width: 0.5rem; - height: 1rem; + right: calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width)); + width: var(--bs-popover-arrow-height); + height: var(--bs-popover-arrow-width); +} +.bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before, .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { + border-width: calc(var(--bs-popover-arrow-width) * 0.5) 0 calc(var(--bs-popover-arrow-width) * 0.5) var(--bs-popover-arrow-height); } .bs-popover-start > .popover-arrow::before, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::before { right: 0; - border-width: 0.5rem 0 0.5rem 0.5rem; - border-left-color: rgba(0, 0, 0, 0.25); + border-left-color: var(--bs-popover-arrow-border); } .bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^=left] > .popover-arrow::after { - right: 1px; - border-width: 0.5rem 0 0.5rem 0.5rem; - border-left-color: #fff; + right: var(--bs-popover-border-width); + border-left-color: var(--bs-popover-bg); } +/* rtl:end:ignore */ .popover-header { - padding: 0.5rem 1rem; + padding: var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x); margin-bottom: 0; - font-size: 1rem; - background-color: #f0f0f0; - border-bottom: 1px solid rgba(0, 0, 0, 0.2); - border-top-left-radius: calc(0.3rem - 1px); - border-top-right-radius: calc(0.3rem - 1px); + font-size: var(--bs-popover-header-font-size); + color: var(--bs-popover-header-color); + background-color: var(--bs-popover-header-bg); + border-bottom: var(--bs-popover-border-width) solid var(--bs-popover-border-color); + border-top-left-radius: var(--bs-popover-inner-border-radius); + border-top-right-radius: var(--bs-popover-inner-border-radius); } .popover-header:empty { display: none; } .popover-body { - padding: 1rem 1rem; - color: #222; + padding: var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x); + color: var(--bs-popover-body-color); } .carousel { @@ -6093,7 +6153,6 @@ textarea.form-control-lg { display: block; } -/* rtl:begin:ignore */ .carousel-item-next:not(.carousel-item-start), .active.carousel-item-end { transform: translateX(100%); @@ -6104,7 +6163,6 @@ textarea.form-control-lg { transform: translateX(-100%); } -/* rtl:end:ignore */ .carousel-fade .carousel-item { opacity: 0; transition-property: opacity; @@ -6124,7 +6182,7 @@ textarea.form-control-lg { } @media (prefers-reduced-motion: reduce) { .carousel-fade .active.carousel-item-start, -.carousel-fade .active.carousel-item-end { + .carousel-fade .active.carousel-item-end { transition: none; } } @@ -6143,13 +6201,14 @@ textarea.form-control-lg { color: #fff; text-align: center; background: none; + filter: var(--bs-carousel-control-icon-filter); border: 0; opacity: 0.5; transition: opacity 0.15s ease; } @media (prefers-reduced-motion: reduce) { .carousel-control-prev, -.carousel-control-next { + .carousel-control-next { transition: none; } } @@ -6180,20 +6239,12 @@ textarea.form-control-lg { background-size: 100% 100%; } -/* rtl:options: { - "autoRename": true, - "stringMap":[ { - "name" : "prev-next", - "search" : "prev", - "replace" : "next" - } ] -} */ .carousel-control-prev-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e"); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")*/; } .carousel-control-next-icon { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e") /*rtl:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")*/; } .carousel-indicators { @@ -6208,7 +6259,6 @@ textarea.form-control-lg { margin-right: 15%; margin-bottom: 1rem; margin-left: 15%; - list-style: none; } .carousel-indicators [data-bs-target] { box-sizing: content-box; @@ -6220,7 +6270,7 @@ textarea.form-control-lg { margin-left: 3px; text-indent: -999px; cursor: pointer; - background-color: #fff; + background-color: var(--bs-carousel-indicator-active-bg); background-clip: padding-box; border: 0; border-top: 10px solid transparent; @@ -6244,25 +6294,38 @@ textarea.form-control-lg { left: 15%; padding-top: 1.25rem; padding-bottom: 1.25rem; - color: #fff; + color: var(--bs-carousel-caption-color); text-align: center; } -.carousel-dark .carousel-control-prev-icon, -.carousel-dark .carousel-control-next-icon { - filter: invert(1) grayscale(100); -} -.carousel-dark .carousel-indicators [data-bs-target] { - background-color: #000; -} -.carousel-dark .carousel-caption { - color: #000; +.carousel-dark { + --bs-carousel-indicator-active-bg: #000; + --bs-carousel-caption-color: #000; + --bs-carousel-control-icon-filter: invert(1) grayscale(100); } -@-webkit-keyframes spinner-border { - to { - transform: rotate(360deg) /* rtl:ignore */; - } +:root, +[data-bs-theme=light] { + --bs-carousel-indicator-active-bg: #fff; + --bs-carousel-caption-color: #fff; + --bs-carousel-control-icon-filter: ; +} + +[data-bs-theme=dark] { + --bs-carousel-indicator-active-bg: #000; + --bs-carousel-caption-color: #000; + --bs-carousel-control-icon-filter: invert(1) grayscale(100); +} + +.spinner-grow, +.spinner-border { + display: inline-block; + flex-shrink: 0; + width: var(--bs-spinner-width); + height: var(--bs-spinner-height); + vertical-align: var(--bs-spinner-vertical-align); + border-radius: 50%; + animation: var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name); } @keyframes spinner-border { @@ -6271,31 +6334,20 @@ textarea.form-control-lg { } } .spinner-border { - display: inline-block; - width: 2rem; - height: 2rem; - vertical-align: -0.125em; - border: 0.25em solid currentColor; + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-vertical-align: -0.125em; + --bs-spinner-border-width: 0.25em; + --bs-spinner-animation-speed: 0.75s; + --bs-spinner-animation-name: spinner-border; + border: var(--bs-spinner-border-width) solid currentcolor; border-right-color: transparent; - border-radius: 50%; - -webkit-animation: 0.75s linear infinite spinner-border; - animation: 0.75s linear infinite spinner-border; } .spinner-border-sm { - width: 1rem; - height: 1rem; - border-width: 0.2em; -} - -@-webkit-keyframes spinner-grow { - 0% { - transform: scale(0); - } - 50% { - opacity: 1; - transform: none; - } + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; + --bs-spinner-border-width: 0.2em; } @keyframes spinner-grow { @@ -6308,47 +6360,487 @@ textarea.form-control-lg { } } .spinner-grow { - display: inline-block; - width: 2rem; - height: 2rem; - vertical-align: -0.125em; - background-color: currentColor; - border-radius: 50%; + --bs-spinner-width: 2rem; + --bs-spinner-height: 2rem; + --bs-spinner-vertical-align: -0.125em; + --bs-spinner-animation-speed: 0.75s; + --bs-spinner-animation-name: spinner-grow; + background-color: currentcolor; opacity: 0; - -webkit-animation: 0.75s linear infinite spinner-grow; - animation: 0.75s linear infinite spinner-grow; } .spinner-grow-sm { - width: 1rem; - height: 1rem; + --bs-spinner-width: 1rem; + --bs-spinner-height: 1rem; } @media (prefers-reduced-motion: reduce) { .spinner-border, -.spinner-grow { - -webkit-animation-duration: 1.5s; - animation-duration: 1.5s; + .spinner-grow { + --bs-spinner-animation-speed: 1.5s; } } +.offcanvas, .offcanvas-xxl, .offcanvas-xl, .offcanvas-lg, .offcanvas-md, .offcanvas-sm { + --bs-offcanvas-zindex: 1045; + --bs-offcanvas-width: 400px; + --bs-offcanvas-height: 30vh; + --bs-offcanvas-padding-x: 1rem; + --bs-offcanvas-padding-y: 1rem; + --bs-offcanvas-color: var(--bs-body-color); + --bs-offcanvas-bg: var(--bs-body-bg); + --bs-offcanvas-border-width: var(--bs-border-width); + --bs-offcanvas-border-color: rgba(0, 0, 0, 0.1); + --bs-offcanvas-box-shadow: var(--bs-box-shadow-sm); + --bs-offcanvas-transition: transform 0.3s ease-in-out; + --bs-offcanvas-title-line-height: 1.5; +} + +@media (max-width: 575.98px) { + .offcanvas-sm { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 575.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-sm { + transition: none; + } +} +@media (max-width: 575.98px) { + .offcanvas-sm.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-sm.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-sm.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-sm.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-sm.showing, .offcanvas-sm.show:not(.hiding) { + transform: none; + } + .offcanvas-sm.showing, .offcanvas-sm.hiding, .offcanvas-sm.show { + visibility: visible; + } +} +@media (min-width: 576px) { + .offcanvas-sm { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-sm .offcanvas-header { + display: none; + } + .offcanvas-sm .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 767.98px) { + .offcanvas-md { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 767.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-md { + transition: none; + } +} +@media (max-width: 767.98px) { + .offcanvas-md.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-md.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-md.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-md.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-md.showing, .offcanvas-md.show:not(.hiding) { + transform: none; + } + .offcanvas-md.showing, .offcanvas-md.hiding, .offcanvas-md.show { + visibility: visible; + } +} +@media (min-width: 768px) { + .offcanvas-md { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-md .offcanvas-header { + display: none; + } + .offcanvas-md .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 991.98px) { + .offcanvas-lg { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 991.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-lg { + transition: none; + } +} +@media (max-width: 991.98px) { + .offcanvas-lg.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-lg.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-lg.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-lg.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-lg.showing, .offcanvas-lg.show:not(.hiding) { + transform: none; + } + .offcanvas-lg.showing, .offcanvas-lg.hiding, .offcanvas-lg.show { + visibility: visible; + } +} +@media (min-width: 992px) { + .offcanvas-lg { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-lg .offcanvas-header { + display: none; + } + .offcanvas-lg .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 1199.98px) { + .offcanvas-xl { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 1199.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-xl { + transition: none; + } +} +@media (max-width: 1199.98px) { + .offcanvas-xl.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-xl.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-xl.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-xl.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-xl.showing, .offcanvas-xl.show:not(.hiding) { + transform: none; + } + .offcanvas-xl.showing, .offcanvas-xl.hiding, .offcanvas-xl.show { + visibility: visible; + } +} +@media (min-width: 1200px) { + .offcanvas-xl { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-xl .offcanvas-header { + display: none; + } + .offcanvas-xl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + +@media (max-width: 1399.98px) { + .offcanvas-xxl { + position: fixed; + bottom: 0; + z-index: var(--bs-offcanvas-zindex); + display: flex; + flex-direction: column; + max-width: 100%; + color: var(--bs-offcanvas-color); + visibility: hidden; + background-color: var(--bs-offcanvas-bg); + background-clip: padding-box; + outline: 0; + transition: var(--bs-offcanvas-transition); + } +} +@media (max-width: 1399.98px) and (prefers-reduced-motion: reduce) { + .offcanvas-xxl { + transition: none; + } +} +@media (max-width: 1399.98px) { + .offcanvas-xxl.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); + } + .offcanvas-xxl.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); + } + .offcanvas-xxl.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); + } + .offcanvas-xxl.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); + } + .offcanvas-xxl.showing, .offcanvas-xxl.show:not(.hiding) { + transform: none; + } + .offcanvas-xxl.showing, .offcanvas-xxl.hiding, .offcanvas-xxl.show { + visibility: visible; + } +} +@media (min-width: 1400px) { + .offcanvas-xxl { + --bs-offcanvas-height: auto; + --bs-offcanvas-border-width: 0; + background-color: transparent !important; + } + .offcanvas-xxl .offcanvas-header { + display: none; + } + .offcanvas-xxl .offcanvas-body { + display: flex; + flex-grow: 0; + padding: 0; + overflow-y: visible; + background-color: transparent !important; + } +} + .offcanvas { position: fixed; bottom: 0; - z-index: 1045; + z-index: var(--bs-offcanvas-zindex); display: flex; flex-direction: column; max-width: 100%; + color: var(--bs-offcanvas-color); visibility: hidden; - background-color: #fff; + background-color: var(--bs-offcanvas-bg); background-clip: padding-box; outline: 0; - transition: transform 0.3s ease-in-out; + transition: var(--bs-offcanvas-transition); } @media (prefers-reduced-motion: reduce) { .offcanvas { transition: none; } } +.offcanvas.offcanvas-start { + top: 0; + left: 0; + width: var(--bs-offcanvas-width); + border-right: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(-100%); +} +.offcanvas.offcanvas-end { + top: 0; + right: 0; + width: var(--bs-offcanvas-width); + border-left: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateX(100%); +} +.offcanvas.offcanvas-top { + top: 0; + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-bottom: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(-100%); +} +.offcanvas.offcanvas-bottom { + right: 0; + left: 0; + height: var(--bs-offcanvas-height); + max-height: 100%; + border-top: var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color); + transform: translateY(100%); +} +.offcanvas.showing, .offcanvas.show:not(.hiding) { + transform: none; +} +.offcanvas.showing, .offcanvas.hiding, .offcanvas.show { + visibility: visible; +} .offcanvas-backdrop { position: fixed; @@ -6369,72 +6861,33 @@ textarea.form-control-lg { .offcanvas-header { display: flex; align-items: center; - justify-content: space-between; - padding: 1rem 1rem; + padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); } .offcanvas-header .btn-close { - padding: 0.5rem 0.5rem; - margin-top: -0.5rem; - margin-right: -0.5rem; - margin-bottom: -0.5rem; + padding: calc(var(--bs-offcanvas-padding-y) * 0.5) calc(var(--bs-offcanvas-padding-x) * 0.5); + margin-top: calc(-0.5 * var(--bs-offcanvas-padding-y)); + margin-right: calc(-0.5 * var(--bs-offcanvas-padding-x)); + margin-bottom: calc(-0.5 * var(--bs-offcanvas-padding-y)); + margin-left: auto; } .offcanvas-title { margin-bottom: 0; - line-height: 1.5; + line-height: var(--bs-offcanvas-title-line-height); } .offcanvas-body { flex-grow: 1; - padding: 1rem 1rem; + padding: var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x); overflow-y: auto; } -.offcanvas-start { - top: 0; - left: 0; - width: 400px; - border-right: 1px solid rgba(0, 0, 0, 0.1); - transform: translateX(-100%); -} - -.offcanvas-end { - top: 0; - right: 0; - width: 400px; - border-left: 1px solid rgba(0, 0, 0, 0.1); - transform: translateX(100%); -} - -.offcanvas-top { - top: 0; - right: 0; - left: 0; - height: 30vh; - max-height: 100%; - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - transform: translateY(-100%); -} - -.offcanvas-bottom { - right: 0; - left: 0; - height: 30vh; - max-height: 100%; - border-top: 1px solid rgba(0, 0, 0, 0.1); - transform: translateY(100%); -} - -.offcanvas.show { - transform: none; -} - .placeholder { display: inline-block; min-height: 1em; vertical-align: middle; cursor: wait; - background-color: currentColor; + background-color: currentcolor; opacity: 0.5; } .placeholder.btn::before { @@ -6455,16 +6908,9 @@ textarea.form-control-lg { } .placeholder-glow .placeholder { - -webkit-animation: placeholder-glow 2s ease-in-out infinite; animation: placeholder-glow 2s ease-in-out infinite; } -@-webkit-keyframes placeholder-glow { - 50% { - opacity: 0.2; - } -} - @keyframes placeholder-glow { 50% { opacity: 0.2; @@ -6475,17 +6921,9 @@ textarea.form-control-lg { mask-image: linear-gradient(130deg, #000 55%, rgba(0, 0, 0, 0.8) 75%, #000 95%); -webkit-mask-size: 200% 100%; mask-size: 200% 100%; - -webkit-animation: placeholder-wave 2s linear infinite; animation: placeholder-wave 2s linear infinite; } -@-webkit-keyframes placeholder-wave { - 100% { - -webkit-mask-position: -200% 0%; - mask-position: -200% 0%; - } -} - @keyframes placeholder-wave { 100% { -webkit-mask-position: -200% 0%; @@ -6498,60 +6936,175 @@ textarea.form-control-lg { content: ""; } +.text-bg-primary { + color: #fff !important; + background-color: RGBA(var(--bs-primary-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-secondary { + color: #000 !important; + background-color: RGBA(var(--bs-secondary-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-success { + color: #fff !important; + background-color: RGBA(var(--bs-success-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-info { + color: #fff !important; + background-color: RGBA(var(--bs-info-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-warning { + color: #fff !important; + background-color: RGBA(var(--bs-warning-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-danger { + color: #fff !important; + background-color: RGBA(var(--bs-danger-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-light { + color: #000 !important; + background-color: RGBA(var(--bs-light-rgb), var(--bs-bg-opacity, 1)) !important; +} + +.text-bg-dark { + color: #fff !important; + background-color: RGBA(var(--bs-dark-rgb), var(--bs-bg-opacity, 1)) !important; +} + .link-primary { - color: #158cba; + color: RGBA(var(--bs-primary-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-primary-rgb), var(--bs-link-underline-opacity, 1)) !important; } .link-primary:hover, .link-primary:focus { - color: #117095; + color: RGBA(17, 112, 149, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(17, 112, 149, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(17, 112, 149, var(--bs-link-underline-opacity, 1)) !important; } .link-secondary { - color: #f0f0f0; + color: RGBA(var(--bs-secondary-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-secondary-rgb), var(--bs-link-underline-opacity, 1)) !important; } .link-secondary:hover, .link-secondary:focus { - color: #f3f3f3; + color: RGBA(243, 243, 243, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(243, 243, 243, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(243, 243, 243, var(--bs-link-underline-opacity, 1)) !important; } .link-success { - color: #28b62c; + color: RGBA(var(--bs-success-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-success-rgb), var(--bs-link-underline-opacity, 1)) !important; } .link-success:hover, .link-success:focus { - color: #209223; + color: RGBA(32, 146, 35, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(32, 146, 35, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(32, 146, 35, var(--bs-link-underline-opacity, 1)) !important; } .link-info { - color: #75caeb; + color: RGBA(var(--bs-info-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-info-rgb), var(--bs-link-underline-opacity, 1)) !important; } .link-info:hover, .link-info:focus { - color: #5ea2bc; + color: RGBA(94, 162, 188, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(94, 162, 188, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(94, 162, 188, var(--bs-link-underline-opacity, 1)) !important; } .link-warning { - color: #ff851b; + color: RGBA(var(--bs-warning-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-warning-rgb), var(--bs-link-underline-opacity, 1)) !important; } .link-warning:hover, .link-warning:focus { - color: #cc6a16; + color: RGBA(204, 106, 22, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(204, 106, 22, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(204, 106, 22, var(--bs-link-underline-opacity, 1)) !important; } .link-danger { - color: #ff4136; + color: RGBA(var(--bs-danger-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-danger-rgb), var(--bs-link-underline-opacity, 1)) !important; } .link-danger:hover, .link-danger:focus { - color: #cc342b; + color: RGBA(204, 52, 43, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(204, 52, 43, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(204, 52, 43, var(--bs-link-underline-opacity, 1)) !important; } .link-light { - color: #f6f6f6; + color: RGBA(var(--bs-light-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-light-rgb), var(--bs-link-underline-opacity, 1)) !important; } .link-light:hover, .link-light:focus { - color: #f8f8f8; + color: RGBA(248, 248, 248, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(248, 248, 248, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(248, 248, 248, var(--bs-link-underline-opacity, 1)) !important; } .link-dark { - color: #555; + color: RGBA(var(--bs-dark-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-dark-rgb), var(--bs-link-underline-opacity, 1)) !important; } .link-dark:hover, .link-dark:focus { - color: #444444; + color: RGBA(68, 68, 68, var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(68, 68, 68, var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(68, 68, 68, var(--bs-link-underline-opacity, 1)) !important; +} + +.link-body-emphasis { + color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 1)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 1)) !important; +} +.link-body-emphasis:hover, .link-body-emphasis:focus { + color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-opacity, 0.75)) !important; + -webkit-text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; + text-decoration-color: RGBA(var(--bs-emphasis-color-rgb), var(--bs-link-underline-opacity, 0.75)) !important; +} + +.focus-ring:focus { + outline: 0; + box-shadow: var(--bs-focus-ring-x, 0) var(--bs-focus-ring-y, 0) var(--bs-focus-ring-blur, 0) var(--bs-focus-ring-width) var(--bs-focus-ring-color); +} + +.icon-link { + display: inline-flex; + gap: 0.375rem; + align-items: center; + -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5)); + text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 0.5)); + text-underline-offset: 0.25em; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; +} +.icon-link > .bi { + flex-shrink: 0; + width: 1em; + height: 1em; + fill: currentcolor; + transition: 0.2s ease-in-out transform; +} +@media (prefers-reduced-motion: reduce) { + .icon-link > .bi { + transition: none; + } +} + +.icon-link-hover:hover > .bi, .icon-link-hover:focus-visible > .bi { + transform: var(--bs-icon-link-transform, translate3d(0.25em, 0, 0)); } .ratio { @@ -6610,6 +7163,13 @@ textarea.form-control-lg { z-index: 1020; } +.sticky-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; +} + @media (min-width: 576px) { .sticky-sm-top { position: -webkit-sticky; @@ -6617,6 +7177,12 @@ textarea.form-control-lg { top: 0; z-index: 1020; } + .sticky-sm-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } } @media (min-width: 768px) { .sticky-md-top { @@ -6625,6 +7191,12 @@ textarea.form-control-lg { top: 0; z-index: 1020; } + .sticky-md-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } } @media (min-width: 992px) { .sticky-lg-top { @@ -6633,6 +7205,12 @@ textarea.form-control-lg { top: 0; z-index: 1020; } + .sticky-lg-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } } @media (min-width: 1200px) { .sticky-xl-top { @@ -6641,6 +7219,12 @@ textarea.form-control-lg { top: 0; z-index: 1020; } + .sticky-xl-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } } @media (min-width: 1400px) { .sticky-xxl-top { @@ -6649,6 +7233,12 @@ textarea.form-control-lg { top: 0; z-index: 1020; } + .sticky-xxl-bottom { + position: -webkit-sticky; + position: sticky; + bottom: 0; + z-index: 1020; + } } .hstack { display: flex; @@ -6666,7 +7256,6 @@ textarea.form-control-lg { .visually-hidden, .visually-hidden-focusable:not(:focus):not(:focus-within) { - position: absolute !important; width: 1px !important; height: 1px !important; padding: 0 !important; @@ -6676,6 +7265,14 @@ textarea.form-control-lg { white-space: nowrap !important; border: 0 !important; } +.visually-hidden:not(caption), +.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption) { + position: absolute !important; +} +.visually-hidden *, +.visually-hidden-focusable:not(:focus):not(:focus-within) * { + overflow: hidden !important; +} .stretched-link::after { position: absolute; @@ -6696,9 +7293,9 @@ textarea.form-control-lg { .vr { display: inline-block; align-self: stretch; - width: 1px; + width: var(--bs-border-width); min-height: 1em; - background-color: currentColor; + background-color: currentcolor; opacity: 0.25; } @@ -6738,6 +7335,31 @@ textarea.form-control-lg { float: none !important; } +.object-fit-contain { + -o-object-fit: contain !important; + object-fit: contain !important; +} + +.object-fit-cover { + -o-object-fit: cover !important; + object-fit: cover !important; +} + +.object-fit-fill { + -o-object-fit: fill !important; + object-fit: fill !important; +} + +.object-fit-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; +} + +.object-fit-none { + -o-object-fit: none !important; + object-fit: none !important; +} + .opacity-0 { opacity: 0 !important; } @@ -6774,6 +7396,38 @@ textarea.form-control-lg { overflow: scroll !important; } +.overflow-x-auto { + overflow-x: auto !important; +} + +.overflow-x-hidden { + overflow-x: hidden !important; +} + +.overflow-x-visible { + overflow-x: visible !important; +} + +.overflow-x-scroll { + overflow-x: scroll !important; +} + +.overflow-y-auto { + overflow-y: auto !important; +} + +.overflow-y-hidden { + overflow-y: hidden !important; +} + +.overflow-y-visible { + overflow-y: visible !important; +} + +.overflow-y-scroll { + overflow-y: scroll !important; +} + .d-inline { display: inline !important; } @@ -6790,6 +7444,10 @@ textarea.form-control-lg { display: grid !important; } +.d-inline-grid { + display: inline-grid !important; +} + .d-table { display: table !important; } @@ -6815,21 +7473,53 @@ textarea.form-control-lg { } .shadow { - box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; + box-shadow: var(--bs-box-shadow) !important; } .shadow-sm { - box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075) !important; + box-shadow: var(--bs-box-shadow-sm) !important; } .shadow-lg { - box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175) !important; + box-shadow: var(--bs-box-shadow-lg) !important; } .shadow-none { box-shadow: none !important; } +.focus-ring-primary { + --bs-focus-ring-color: rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-secondary { + --bs-focus-ring-color: rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-success { + --bs-focus-ring-color: rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-info { + --bs-focus-ring-color: rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-warning { + --bs-focus-ring-color: rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-danger { + --bs-focus-ring-color: rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-light { + --bs-focus-ring-color: rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity)); +} + +.focus-ring-dark { + --bs-focus-ring-color: rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity)); +} + .position-static { position: static !important; } @@ -6912,7 +7602,7 @@ textarea.form-control-lg { } .border { - border: 1px solid #dee2e6 !important; + border: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; } .border-0 { @@ -6920,7 +7610,7 @@ textarea.form-control-lg { } .border-top { - border-top: 1px solid #dee2e6 !important; + border-top: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; } .border-top-0 { @@ -6928,7 +7618,7 @@ textarea.form-control-lg { } .border-end { - border-right: 1px solid #dee2e6 !important; + border-right: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; } .border-end-0 { @@ -6936,7 +7626,7 @@ textarea.form-control-lg { } .border-bottom { - border-bottom: 1px solid #dee2e6 !important; + border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; } .border-bottom-0 { @@ -6944,7 +7634,7 @@ textarea.form-control-lg { } .border-start { - border-left: 1px solid #dee2e6 !important; + border-left: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color) !important; } .border-start-0 { @@ -6952,39 +7642,85 @@ textarea.form-control-lg { } .border-primary { - border-color: #158cba !important; + --bs-border-opacity: 1; + border-color: rgba(var(--bs-primary-rgb), var(--bs-border-opacity)) !important; } .border-secondary { - border-color: #f0f0f0 !important; + --bs-border-opacity: 1; + border-color: rgba(var(--bs-secondary-rgb), var(--bs-border-opacity)) !important; } .border-success { - border-color: #28b62c !important; + --bs-border-opacity: 1; + border-color: rgba(var(--bs-success-rgb), var(--bs-border-opacity)) !important; } .border-info { - border-color: #75caeb !important; + --bs-border-opacity: 1; + border-color: rgba(var(--bs-info-rgb), var(--bs-border-opacity)) !important; } .border-warning { - border-color: #ff851b !important; + --bs-border-opacity: 1; + border-color: rgba(var(--bs-warning-rgb), var(--bs-border-opacity)) !important; } .border-danger { - border-color: #ff4136 !important; + --bs-border-opacity: 1; + border-color: rgba(var(--bs-danger-rgb), var(--bs-border-opacity)) !important; } .border-light { - border-color: #f6f6f6 !important; + --bs-border-opacity: 1; + border-color: rgba(var(--bs-light-rgb), var(--bs-border-opacity)) !important; } .border-dark { - border-color: #555 !important; + --bs-border-opacity: 1; + border-color: rgba(var(--bs-dark-rgb), var(--bs-border-opacity)) !important; +} + +.border-black { + --bs-border-opacity: 1; + border-color: rgba(var(--bs-black-rgb), var(--bs-border-opacity)) !important; } .border-white { - border-color: #fff !important; + --bs-border-opacity: 1; + border-color: rgba(var(--bs-white-rgb), var(--bs-border-opacity)) !important; +} + +.border-primary-subtle { + border-color: var(--bs-primary-border-subtle) !important; +} + +.border-secondary-subtle { + border-color: var(--bs-secondary-border-subtle) !important; +} + +.border-success-subtle { + border-color: var(--bs-success-border-subtle) !important; +} + +.border-info-subtle { + border-color: var(--bs-info-border-subtle) !important; +} + +.border-warning-subtle { + border-color: var(--bs-warning-border-subtle) !important; +} + +.border-danger-subtle { + border-color: var(--bs-danger-border-subtle) !important; +} + +.border-light-subtle { + border-color: var(--bs-light-border-subtle) !important; +} + +.border-dark-subtle { + border-color: var(--bs-dark-border-subtle) !important; } .border-1 { @@ -7007,6 +7743,26 @@ textarea.form-control-lg { border-width: 5px !important; } +.border-opacity-10 { + --bs-border-opacity: 0.1; +} + +.border-opacity-25 { + --bs-border-opacity: 0.25; +} + +.border-opacity-50 { + --bs-border-opacity: 0.5; +} + +.border-opacity-75 { + --bs-border-opacity: 0.75; +} + +.border-opacity-100 { + --bs-border-opacity: 1; +} + .w-25 { width: 25% !important; } @@ -7119,30 +7875,6 @@ textarea.form-control-lg { flex-wrap: wrap-reverse !important; } -.gap-0 { - gap: 0 !important; -} - -.gap-1 { - gap: 0.25rem !important; -} - -.gap-2 { - gap: 0.5rem !important; -} - -.gap-3 { - gap: 1rem !important; -} - -.gap-4 { - gap: 1.5rem !important; -} - -.gap-5 { - gap: 3rem !important; -} - .justify-content-start { justify-content: flex-start !important; } @@ -7657,6 +8389,84 @@ textarea.form-control-lg { padding-left: 3rem !important; } +.gap-0 { + gap: 0 !important; +} + +.gap-1 { + gap: 0.25rem !important; +} + +.gap-2 { + gap: 0.5rem !important; +} + +.gap-3 { + gap: 1rem !important; +} + +.gap-4 { + gap: 1.5rem !important; +} + +.gap-5 { + gap: 3rem !important; +} + +.row-gap-0 { + row-gap: 0 !important; +} + +.row-gap-1 { + row-gap: 0.25rem !important; +} + +.row-gap-2 { + row-gap: 0.5rem !important; +} + +.row-gap-3 { + row-gap: 1rem !important; +} + +.row-gap-4 { + row-gap: 1.5rem !important; +} + +.row-gap-5 { + row-gap: 3rem !important; +} + +.column-gap-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; +} + +.column-gap-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; +} + +.column-gap-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; +} + +.column-gap-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; +} + +.column-gap-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; +} + +.column-gap-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; +} + .font-monospace { font-family: var(--bs-font-monospace) !important; } @@ -7693,18 +8503,26 @@ textarea.form-control-lg { font-style: normal !important; } -.fw-light { - font-weight: 300 !important; -} - .fw-lighter { font-weight: lighter !important; } +.fw-light { + font-weight: 300 !important; +} + .fw-normal { font-weight: 400 !important; } +.fw-medium { + font-weight: 500 !important; +} + +.fw-semibold { + font-weight: 600 !important; +} + .fw-bold { font-weight: 700 !important; } @@ -7837,7 +8655,7 @@ textarea.form-control-lg { .text-muted { --bs-text-opacity: 1; - color: #999 !important; + color: var(--bs-secondary-color) !important; } .text-black-50 { @@ -7850,6 +8668,21 @@ textarea.form-control-lg { color: rgba(255, 255, 255, 0.5) !important; } +.text-body-secondary { + --bs-text-opacity: 1; + color: var(--bs-secondary-color) !important; +} + +.text-body-tertiary { + --bs-text-opacity: 1; + color: var(--bs-tertiary-color) !important; +} + +.text-body-emphasis { + --bs-text-opacity: 1; + color: var(--bs-emphasis-color) !important; +} + .text-reset { --bs-text-opacity: 1; color: inherit !important; @@ -7871,6 +8704,204 @@ textarea.form-control-lg { --bs-text-opacity: 1; } +.text-primary-emphasis { + color: var(--bs-primary-text-emphasis) !important; +} + +.text-secondary-emphasis { + color: var(--bs-secondary-text-emphasis) !important; +} + +.text-success-emphasis { + color: var(--bs-success-text-emphasis) !important; +} + +.text-info-emphasis { + color: var(--bs-info-text-emphasis) !important; +} + +.text-warning-emphasis { + color: var(--bs-warning-text-emphasis) !important; +} + +.text-danger-emphasis { + color: var(--bs-danger-text-emphasis) !important; +} + +.text-light-emphasis { + color: var(--bs-light-text-emphasis) !important; +} + +.text-dark-emphasis { + color: var(--bs-dark-text-emphasis) !important; +} + +.link-opacity-10 { + --bs-link-opacity: 0.1; +} + +.link-opacity-10-hover:hover { + --bs-link-opacity: 0.1; +} + +.link-opacity-25 { + --bs-link-opacity: 0.25; +} + +.link-opacity-25-hover:hover { + --bs-link-opacity: 0.25; +} + +.link-opacity-50 { + --bs-link-opacity: 0.5; +} + +.link-opacity-50-hover:hover { + --bs-link-opacity: 0.5; +} + +.link-opacity-75 { + --bs-link-opacity: 0.75; +} + +.link-opacity-75-hover:hover { + --bs-link-opacity: 0.75; +} + +.link-opacity-100 { + --bs-link-opacity: 1; +} + +.link-opacity-100-hover:hover { + --bs-link-opacity: 1; +} + +.link-offset-1 { + text-underline-offset: 0.125em !important; +} + +.link-offset-1-hover:hover { + text-underline-offset: 0.125em !important; +} + +.link-offset-2 { + text-underline-offset: 0.25em !important; +} + +.link-offset-2-hover:hover { + text-underline-offset: 0.25em !important; +} + +.link-offset-3 { + text-underline-offset: 0.375em !important; +} + +.link-offset-3-hover:hover { + text-underline-offset: 0.375em !important; +} + +.link-underline-primary { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-primary-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-secondary { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-secondary-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-success { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-success-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-info { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-info-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-warning { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-warning-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-danger { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-danger-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-light { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-light-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline-dark { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; + text-decoration-color: rgba(var(--bs-dark-rgb), var(--bs-link-underline-opacity)) !important; +} + +.link-underline { + --bs-link-underline-opacity: 1; + -webkit-text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; + text-decoration-color: rgba(var(--bs-link-color-rgb), var(--bs-link-underline-opacity, 1)) !important; +} + +.link-underline-opacity-0 { + --bs-link-underline-opacity: 0; +} + +.link-underline-opacity-0-hover:hover { + --bs-link-underline-opacity: 0; +} + +.link-underline-opacity-10 { + --bs-link-underline-opacity: 0.1; +} + +.link-underline-opacity-10-hover:hover { + --bs-link-underline-opacity: 0.1; +} + +.link-underline-opacity-25 { + --bs-link-underline-opacity: 0.25; +} + +.link-underline-opacity-25-hover:hover { + --bs-link-underline-opacity: 0.25; +} + +.link-underline-opacity-50 { + --bs-link-underline-opacity: 0.5; +} + +.link-underline-opacity-50-hover:hover { + --bs-link-underline-opacity: 0.5; +} + +.link-underline-opacity-75 { + --bs-link-underline-opacity: 0.75; +} + +.link-underline-opacity-75-hover:hover { + --bs-link-underline-opacity: 0.75; +} + +.link-underline-opacity-100 { + --bs-link-underline-opacity: 1; +} + +.link-underline-opacity-100-hover:hover { + --bs-link-underline-opacity: 1; +} + .bg-primary { --bs-bg-opacity: 1; background-color: rgba(var(--bs-primary-rgb), var(--bs-bg-opacity)) !important; @@ -7931,6 +8962,16 @@ textarea.form-control-lg { background-color: transparent !important; } +.bg-body-secondary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-secondary-bg-rgb), var(--bs-bg-opacity)) !important; +} + +.bg-body-tertiary { + --bs-bg-opacity: 1; + background-color: rgba(var(--bs-tertiary-bg-rgb), var(--bs-bg-opacity)) !important; +} + .bg-opacity-10 { --bs-bg-opacity: 0.1; } @@ -7951,6 +8992,38 @@ textarea.form-control-lg { --bs-bg-opacity: 1; } +.bg-primary-subtle { + background-color: var(--bs-primary-bg-subtle) !important; +} + +.bg-secondary-subtle { + background-color: var(--bs-secondary-bg-subtle) !important; +} + +.bg-success-subtle { + background-color: var(--bs-success-bg-subtle) !important; +} + +.bg-info-subtle { + background-color: var(--bs-info-bg-subtle) !important; +} + +.bg-warning-subtle { + background-color: var(--bs-warning-bg-subtle) !important; +} + +.bg-danger-subtle { + background-color: var(--bs-danger-bg-subtle) !important; +} + +.bg-light-subtle { + background-color: var(--bs-light-bg-subtle) !important; +} + +.bg-dark-subtle { + background-color: var(--bs-dark-bg-subtle) !important; +} + .bg-gradient { background-image: var(--bs-gradient) !important; } @@ -7982,7 +9055,7 @@ textarea.form-control-lg { } .rounded { - border-radius: 0.25rem !important; + border-radius: var(--bs-border-radius) !important; } .rounded-0 { @@ -7990,15 +9063,23 @@ textarea.form-control-lg { } .rounded-1 { - border-radius: 0.2rem !important; + border-radius: var(--bs-border-radius-sm) !important; } .rounded-2 { - border-radius: 0.25rem !important; + border-radius: var(--bs-border-radius) !important; } .rounded-3 { - border-radius: 0.3rem !important; + border-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-4 { + border-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-5 { + border-radius: var(--bs-border-radius-xxl) !important; } .rounded-circle { @@ -8006,27 +9087,187 @@ textarea.form-control-lg { } .rounded-pill { - border-radius: 50rem !important; + border-radius: var(--bs-border-radius-pill) !important; } .rounded-top { - border-top-left-radius: 0.25rem !important; - border-top-right-radius: 0.25rem !important; + border-top-left-radius: var(--bs-border-radius) !important; + border-top-right-radius: var(--bs-border-radius) !important; +} + +.rounded-top-0 { + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; +} + +.rounded-top-1 { + border-top-left-radius: var(--bs-border-radius-sm) !important; + border-top-right-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-top-2 { + border-top-left-radius: var(--bs-border-radius) !important; + border-top-right-radius: var(--bs-border-radius) !important; +} + +.rounded-top-3 { + border-top-left-radius: var(--bs-border-radius-lg) !important; + border-top-right-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-top-4 { + border-top-left-radius: var(--bs-border-radius-xl) !important; + border-top-right-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-top-5 { + border-top-left-radius: var(--bs-border-radius-xxl) !important; + border-top-right-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-top-circle { + border-top-left-radius: 50% !important; + border-top-right-radius: 50% !important; +} + +.rounded-top-pill { + border-top-left-radius: var(--bs-border-radius-pill) !important; + border-top-right-radius: var(--bs-border-radius-pill) !important; } .rounded-end { - border-top-right-radius: 0.25rem !important; - border-bottom-right-radius: 0.25rem !important; + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; +} + +.rounded-end-0 { + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; +} + +.rounded-end-1 { + border-top-right-radius: var(--bs-border-radius-sm) !important; + border-bottom-right-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-end-2 { + border-top-right-radius: var(--bs-border-radius) !important; + border-bottom-right-radius: var(--bs-border-radius) !important; +} + +.rounded-end-3 { + border-top-right-radius: var(--bs-border-radius-lg) !important; + border-bottom-right-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-end-4 { + border-top-right-radius: var(--bs-border-radius-xl) !important; + border-bottom-right-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-end-5 { + border-top-right-radius: var(--bs-border-radius-xxl) !important; + border-bottom-right-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-end-circle { + border-top-right-radius: 50% !important; + border-bottom-right-radius: 50% !important; +} + +.rounded-end-pill { + border-top-right-radius: var(--bs-border-radius-pill) !important; + border-bottom-right-radius: var(--bs-border-radius-pill) !important; } .rounded-bottom { - border-bottom-right-radius: 0.25rem !important; - border-bottom-left-radius: 0.25rem !important; + border-bottom-right-radius: var(--bs-border-radius) !important; + border-bottom-left-radius: var(--bs-border-radius) !important; +} + +.rounded-bottom-0 { + border-bottom-right-radius: 0 !important; + border-bottom-left-radius: 0 !important; +} + +.rounded-bottom-1 { + border-bottom-right-radius: var(--bs-border-radius-sm) !important; + border-bottom-left-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-bottom-2 { + border-bottom-right-radius: var(--bs-border-radius) !important; + border-bottom-left-radius: var(--bs-border-radius) !important; +} + +.rounded-bottom-3 { + border-bottom-right-radius: var(--bs-border-radius-lg) !important; + border-bottom-left-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-bottom-4 { + border-bottom-right-radius: var(--bs-border-radius-xl) !important; + border-bottom-left-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-bottom-5 { + border-bottom-right-radius: var(--bs-border-radius-xxl) !important; + border-bottom-left-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-bottom-circle { + border-bottom-right-radius: 50% !important; + border-bottom-left-radius: 50% !important; +} + +.rounded-bottom-pill { + border-bottom-right-radius: var(--bs-border-radius-pill) !important; + border-bottom-left-radius: var(--bs-border-radius-pill) !important; } .rounded-start { - border-bottom-left-radius: 0.25rem !important; - border-top-left-radius: 0.25rem !important; + border-bottom-left-radius: var(--bs-border-radius) !important; + border-top-left-radius: var(--bs-border-radius) !important; +} + +.rounded-start-0 { + border-bottom-left-radius: 0 !important; + border-top-left-radius: 0 !important; +} + +.rounded-start-1 { + border-bottom-left-radius: var(--bs-border-radius-sm) !important; + border-top-left-radius: var(--bs-border-radius-sm) !important; +} + +.rounded-start-2 { + border-bottom-left-radius: var(--bs-border-radius) !important; + border-top-left-radius: var(--bs-border-radius) !important; +} + +.rounded-start-3 { + border-bottom-left-radius: var(--bs-border-radius-lg) !important; + border-top-left-radius: var(--bs-border-radius-lg) !important; +} + +.rounded-start-4 { + border-bottom-left-radius: var(--bs-border-radius-xl) !important; + border-top-left-radius: var(--bs-border-radius-xl) !important; +} + +.rounded-start-5 { + border-bottom-left-radius: var(--bs-border-radius-xxl) !important; + border-top-left-radius: var(--bs-border-radius-xxl) !important; +} + +.rounded-start-circle { + border-bottom-left-radius: 50% !important; + border-top-left-radius: 50% !important; +} + +.rounded-start-pill { + border-bottom-left-radius: var(--bs-border-radius-pill) !important; + border-top-left-radius: var(--bs-border-radius-pill) !important; } .visible { @@ -8037,653 +9278,583 @@ textarea.form-control-lg { visibility: hidden !important; } +.z-n1 { + z-index: -1 !important; +} + +.z-0 { + z-index: 0 !important; +} + +.z-1 { + z-index: 1 !important; +} + +.z-2 { + z-index: 2 !important; +} + +.z-3 { + z-index: 3 !important; +} + @media (min-width: 576px) { .float-sm-start { float: left !important; } - .float-sm-end { float: right !important; } - .float-sm-none { float: none !important; } - + .object-fit-sm-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-sm-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-sm-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-sm-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-sm-none { + -o-object-fit: none !important; + object-fit: none !important; + } .d-sm-inline { display: inline !important; } - .d-sm-inline-block { display: inline-block !important; } - .d-sm-block { display: block !important; } - .d-sm-grid { display: grid !important; } - + .d-sm-inline-grid { + display: inline-grid !important; + } .d-sm-table { display: table !important; } - .d-sm-table-row { display: table-row !important; } - .d-sm-table-cell { display: table-cell !important; } - .d-sm-flex { display: flex !important; } - .d-sm-inline-flex { display: inline-flex !important; } - .d-sm-none { display: none !important; } - .flex-sm-fill { flex: 1 1 auto !important; } - .flex-sm-row { flex-direction: row !important; } - .flex-sm-column { flex-direction: column !important; } - .flex-sm-row-reverse { flex-direction: row-reverse !important; } - .flex-sm-column-reverse { flex-direction: column-reverse !important; } - .flex-sm-grow-0 { flex-grow: 0 !important; } - .flex-sm-grow-1 { flex-grow: 1 !important; } - .flex-sm-shrink-0 { flex-shrink: 0 !important; } - .flex-sm-shrink-1 { flex-shrink: 1 !important; } - .flex-sm-wrap { flex-wrap: wrap !important; } - .flex-sm-nowrap { flex-wrap: nowrap !important; } - .flex-sm-wrap-reverse { flex-wrap: wrap-reverse !important; } - - .gap-sm-0 { - gap: 0 !important; - } - - .gap-sm-1 { - gap: 0.25rem !important; - } - - .gap-sm-2 { - gap: 0.5rem !important; - } - - .gap-sm-3 { - gap: 1rem !important; - } - - .gap-sm-4 { - gap: 1.5rem !important; - } - - .gap-sm-5 { - gap: 3rem !important; - } - .justify-content-sm-start { justify-content: flex-start !important; } - .justify-content-sm-end { justify-content: flex-end !important; } - .justify-content-sm-center { justify-content: center !important; } - .justify-content-sm-between { justify-content: space-between !important; } - .justify-content-sm-around { justify-content: space-around !important; } - .justify-content-sm-evenly { justify-content: space-evenly !important; } - .align-items-sm-start { align-items: flex-start !important; } - .align-items-sm-end { align-items: flex-end !important; } - .align-items-sm-center { align-items: center !important; } - .align-items-sm-baseline { align-items: baseline !important; } - .align-items-sm-stretch { align-items: stretch !important; } - .align-content-sm-start { align-content: flex-start !important; } - .align-content-sm-end { align-content: flex-end !important; } - .align-content-sm-center { align-content: center !important; } - .align-content-sm-between { align-content: space-between !important; } - .align-content-sm-around { align-content: space-around !important; } - .align-content-sm-stretch { align-content: stretch !important; } - .align-self-sm-auto { align-self: auto !important; } - .align-self-sm-start { align-self: flex-start !important; } - .align-self-sm-end { align-self: flex-end !important; } - .align-self-sm-center { align-self: center !important; } - .align-self-sm-baseline { align-self: baseline !important; } - .align-self-sm-stretch { align-self: stretch !important; } - .order-sm-first { order: -1 !important; } - .order-sm-0 { order: 0 !important; } - .order-sm-1 { order: 1 !important; } - .order-sm-2 { order: 2 !important; } - .order-sm-3 { order: 3 !important; } - .order-sm-4 { order: 4 !important; } - .order-sm-5 { order: 5 !important; } - .order-sm-last { order: 6 !important; } - .m-sm-0 { margin: 0 !important; } - .m-sm-1 { margin: 0.25rem !important; } - .m-sm-2 { margin: 0.5rem !important; } - .m-sm-3 { margin: 1rem !important; } - .m-sm-4 { margin: 1.5rem !important; } - .m-sm-5 { margin: 3rem !important; } - .m-sm-auto { margin: auto !important; } - .mx-sm-0 { margin-right: 0 !important; margin-left: 0 !important; } - .mx-sm-1 { margin-right: 0.25rem !important; margin-left: 0.25rem !important; } - .mx-sm-2 { margin-right: 0.5rem !important; margin-left: 0.5rem !important; } - .mx-sm-3 { margin-right: 1rem !important; margin-left: 1rem !important; } - .mx-sm-4 { margin-right: 1.5rem !important; margin-left: 1.5rem !important; } - .mx-sm-5 { margin-right: 3rem !important; margin-left: 3rem !important; } - .mx-sm-auto { margin-right: auto !important; margin-left: auto !important; } - .my-sm-0 { margin-top: 0 !important; margin-bottom: 0 !important; } - .my-sm-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; } - .my-sm-2 { margin-top: 0.5rem !important; margin-bottom: 0.5rem !important; } - .my-sm-3 { margin-top: 1rem !important; margin-bottom: 1rem !important; } - .my-sm-4 { margin-top: 1.5rem !important; margin-bottom: 1.5rem !important; } - .my-sm-5 { margin-top: 3rem !important; margin-bottom: 3rem !important; } - .my-sm-auto { margin-top: auto !important; margin-bottom: auto !important; } - .mt-sm-0 { margin-top: 0 !important; } - .mt-sm-1 { margin-top: 0.25rem !important; } - .mt-sm-2 { margin-top: 0.5rem !important; } - .mt-sm-3 { margin-top: 1rem !important; } - .mt-sm-4 { margin-top: 1.5rem !important; } - .mt-sm-5 { margin-top: 3rem !important; } - .mt-sm-auto { margin-top: auto !important; } - .me-sm-0 { margin-right: 0 !important; } - .me-sm-1 { margin-right: 0.25rem !important; } - .me-sm-2 { margin-right: 0.5rem !important; } - .me-sm-3 { margin-right: 1rem !important; } - .me-sm-4 { margin-right: 1.5rem !important; } - .me-sm-5 { margin-right: 3rem !important; } - .me-sm-auto { margin-right: auto !important; } - .mb-sm-0 { margin-bottom: 0 !important; } - .mb-sm-1 { margin-bottom: 0.25rem !important; } - .mb-sm-2 { margin-bottom: 0.5rem !important; } - .mb-sm-3 { margin-bottom: 1rem !important; } - .mb-sm-4 { margin-bottom: 1.5rem !important; } - .mb-sm-5 { margin-bottom: 3rem !important; } - .mb-sm-auto { margin-bottom: auto !important; } - .ms-sm-0 { margin-left: 0 !important; } - .ms-sm-1 { margin-left: 0.25rem !important; } - .ms-sm-2 { margin-left: 0.5rem !important; } - .ms-sm-3 { margin-left: 1rem !important; } - .ms-sm-4 { margin-left: 1.5rem !important; } - .ms-sm-5 { margin-left: 3rem !important; } - .ms-sm-auto { margin-left: auto !important; } - .p-sm-0 { padding: 0 !important; } - .p-sm-1 { padding: 0.25rem !important; } - .p-sm-2 { padding: 0.5rem !important; } - .p-sm-3 { padding: 1rem !important; } - .p-sm-4 { padding: 1.5rem !important; } - .p-sm-5 { padding: 3rem !important; } - .px-sm-0 { padding-right: 0 !important; padding-left: 0 !important; } - .px-sm-1 { padding-right: 0.25rem !important; padding-left: 0.25rem !important; } - .px-sm-2 { padding-right: 0.5rem !important; padding-left: 0.5rem !important; } - .px-sm-3 { padding-right: 1rem !important; padding-left: 1rem !important; } - .px-sm-4 { padding-right: 1.5rem !important; padding-left: 1.5rem !important; } - .px-sm-5 { padding-right: 3rem !important; padding-left: 3rem !important; } - .py-sm-0 { padding-top: 0 !important; padding-bottom: 0 !important; } - .py-sm-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; } - .py-sm-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; } - .py-sm-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; } - .py-sm-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; } - .py-sm-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; } - .pt-sm-0 { padding-top: 0 !important; } - .pt-sm-1 { padding-top: 0.25rem !important; } - .pt-sm-2 { padding-top: 0.5rem !important; } - .pt-sm-3 { padding-top: 1rem !important; } - .pt-sm-4 { padding-top: 1.5rem !important; } - .pt-sm-5 { padding-top: 3rem !important; } - .pe-sm-0 { padding-right: 0 !important; } - .pe-sm-1 { padding-right: 0.25rem !important; } - .pe-sm-2 { padding-right: 0.5rem !important; } - .pe-sm-3 { padding-right: 1rem !important; } - .pe-sm-4 { padding-right: 1.5rem !important; } - .pe-sm-5 { padding-right: 3rem !important; } - .pb-sm-0 { padding-bottom: 0 !important; } - .pb-sm-1 { padding-bottom: 0.25rem !important; } - .pb-sm-2 { padding-bottom: 0.5rem !important; } - .pb-sm-3 { padding-bottom: 1rem !important; } - .pb-sm-4 { padding-bottom: 1.5rem !important; } - .pb-sm-5 { padding-bottom: 3rem !important; } - .ps-sm-0 { padding-left: 0 !important; } - .ps-sm-1 { padding-left: 0.25rem !important; } - .ps-sm-2 { padding-left: 0.5rem !important; } - .ps-sm-3 { padding-left: 1rem !important; } - .ps-sm-4 { padding-left: 1.5rem !important; } - .ps-sm-5 { padding-left: 3rem !important; } - + .gap-sm-0 { + gap: 0 !important; + } + .gap-sm-1 { + gap: 0.25rem !important; + } + .gap-sm-2 { + gap: 0.5rem !important; + } + .gap-sm-3 { + gap: 1rem !important; + } + .gap-sm-4 { + gap: 1.5rem !important; + } + .gap-sm-5 { + gap: 3rem !important; + } + .row-gap-sm-0 { + row-gap: 0 !important; + } + .row-gap-sm-1 { + row-gap: 0.25rem !important; + } + .row-gap-sm-2 { + row-gap: 0.5rem !important; + } + .row-gap-sm-3 { + row-gap: 1rem !important; + } + .row-gap-sm-4 { + row-gap: 1.5rem !important; + } + .row-gap-sm-5 { + row-gap: 3rem !important; + } + .column-gap-sm-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-sm-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-sm-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-sm-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-sm-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-sm-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } .text-sm-start { text-align: left !important; } - .text-sm-end { text-align: right !important; } - .text-sm-center { text-align: center !important; } @@ -8692,649 +9863,559 @@ textarea.form-control-lg { .float-md-start { float: left !important; } - .float-md-end { float: right !important; } - .float-md-none { float: none !important; } - + .object-fit-md-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-md-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-md-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-md-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-md-none { + -o-object-fit: none !important; + object-fit: none !important; + } .d-md-inline { display: inline !important; } - .d-md-inline-block { display: inline-block !important; } - .d-md-block { display: block !important; } - .d-md-grid { display: grid !important; } - + .d-md-inline-grid { + display: inline-grid !important; + } .d-md-table { display: table !important; } - .d-md-table-row { display: table-row !important; } - .d-md-table-cell { display: table-cell !important; } - .d-md-flex { display: flex !important; } - .d-md-inline-flex { display: inline-flex !important; } - .d-md-none { display: none !important; } - .flex-md-fill { flex: 1 1 auto !important; } - .flex-md-row { flex-direction: row !important; } - .flex-md-column { flex-direction: column !important; } - .flex-md-row-reverse { flex-direction: row-reverse !important; } - .flex-md-column-reverse { flex-direction: column-reverse !important; } - .flex-md-grow-0 { flex-grow: 0 !important; } - .flex-md-grow-1 { flex-grow: 1 !important; } - .flex-md-shrink-0 { flex-shrink: 0 !important; } - .flex-md-shrink-1 { flex-shrink: 1 !important; } - .flex-md-wrap { flex-wrap: wrap !important; } - .flex-md-nowrap { flex-wrap: nowrap !important; } - .flex-md-wrap-reverse { flex-wrap: wrap-reverse !important; } - - .gap-md-0 { - gap: 0 !important; - } - - .gap-md-1 { - gap: 0.25rem !important; - } - - .gap-md-2 { - gap: 0.5rem !important; - } - - .gap-md-3 { - gap: 1rem !important; - } - - .gap-md-4 { - gap: 1.5rem !important; - } - - .gap-md-5 { - gap: 3rem !important; - } - .justify-content-md-start { justify-content: flex-start !important; } - .justify-content-md-end { justify-content: flex-end !important; } - .justify-content-md-center { justify-content: center !important; } - .justify-content-md-between { justify-content: space-between !important; } - .justify-content-md-around { justify-content: space-around !important; } - .justify-content-md-evenly { justify-content: space-evenly !important; } - .align-items-md-start { align-items: flex-start !important; } - .align-items-md-end { align-items: flex-end !important; } - .align-items-md-center { align-items: center !important; } - .align-items-md-baseline { align-items: baseline !important; } - .align-items-md-stretch { align-items: stretch !important; } - .align-content-md-start { align-content: flex-start !important; } - .align-content-md-end { align-content: flex-end !important; } - .align-content-md-center { align-content: center !important; } - .align-content-md-between { align-content: space-between !important; } - .align-content-md-around { align-content: space-around !important; } - .align-content-md-stretch { align-content: stretch !important; } - .align-self-md-auto { align-self: auto !important; } - .align-self-md-start { align-self: flex-start !important; } - .align-self-md-end { align-self: flex-end !important; } - .align-self-md-center { align-self: center !important; } - .align-self-md-baseline { align-self: baseline !important; } - .align-self-md-stretch { align-self: stretch !important; } - .order-md-first { order: -1 !important; } - .order-md-0 { order: 0 !important; } - .order-md-1 { order: 1 !important; } - .order-md-2 { order: 2 !important; } - .order-md-3 { order: 3 !important; } - .order-md-4 { order: 4 !important; } - .order-md-5 { order: 5 !important; } - .order-md-last { order: 6 !important; } - .m-md-0 { margin: 0 !important; } - .m-md-1 { margin: 0.25rem !important; } - .m-md-2 { margin: 0.5rem !important; } - .m-md-3 { margin: 1rem !important; } - .m-md-4 { margin: 1.5rem !important; } - .m-md-5 { margin: 3rem !important; } - .m-md-auto { margin: auto !important; } - .mx-md-0 { margin-right: 0 !important; margin-left: 0 !important; } - .mx-md-1 { margin-right: 0.25rem !important; margin-left: 0.25rem !important; } - .mx-md-2 { margin-right: 0.5rem !important; margin-left: 0.5rem !important; } - .mx-md-3 { margin-right: 1rem !important; margin-left: 1rem !important; } - .mx-md-4 { margin-right: 1.5rem !important; margin-left: 1.5rem !important; } - .mx-md-5 { margin-right: 3rem !important; margin-left: 3rem !important; } - .mx-md-auto { margin-right: auto !important; margin-left: auto !important; } - .my-md-0 { margin-top: 0 !important; margin-bottom: 0 !important; } - .my-md-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; } - .my-md-2 { margin-top: 0.5rem !important; margin-bottom: 0.5rem !important; } - .my-md-3 { margin-top: 1rem !important; margin-bottom: 1rem !important; } - .my-md-4 { margin-top: 1.5rem !important; margin-bottom: 1.5rem !important; } - .my-md-5 { margin-top: 3rem !important; margin-bottom: 3rem !important; } - .my-md-auto { margin-top: auto !important; margin-bottom: auto !important; } - .mt-md-0 { margin-top: 0 !important; } - .mt-md-1 { margin-top: 0.25rem !important; } - .mt-md-2 { margin-top: 0.5rem !important; } - .mt-md-3 { margin-top: 1rem !important; } - .mt-md-4 { margin-top: 1.5rem !important; } - .mt-md-5 { margin-top: 3rem !important; } - .mt-md-auto { margin-top: auto !important; } - .me-md-0 { margin-right: 0 !important; } - .me-md-1 { margin-right: 0.25rem !important; } - .me-md-2 { margin-right: 0.5rem !important; } - .me-md-3 { margin-right: 1rem !important; } - .me-md-4 { margin-right: 1.5rem !important; } - .me-md-5 { margin-right: 3rem !important; } - .me-md-auto { margin-right: auto !important; } - .mb-md-0 { margin-bottom: 0 !important; } - .mb-md-1 { margin-bottom: 0.25rem !important; } - .mb-md-2 { margin-bottom: 0.5rem !important; } - .mb-md-3 { margin-bottom: 1rem !important; } - .mb-md-4 { margin-bottom: 1.5rem !important; } - .mb-md-5 { margin-bottom: 3rem !important; } - .mb-md-auto { margin-bottom: auto !important; } - .ms-md-0 { margin-left: 0 !important; } - .ms-md-1 { margin-left: 0.25rem !important; } - .ms-md-2 { margin-left: 0.5rem !important; } - .ms-md-3 { margin-left: 1rem !important; } - .ms-md-4 { margin-left: 1.5rem !important; } - .ms-md-5 { margin-left: 3rem !important; } - .ms-md-auto { margin-left: auto !important; } - .p-md-0 { padding: 0 !important; } - .p-md-1 { padding: 0.25rem !important; } - .p-md-2 { padding: 0.5rem !important; } - .p-md-3 { padding: 1rem !important; } - .p-md-4 { padding: 1.5rem !important; } - .p-md-5 { padding: 3rem !important; } - .px-md-0 { padding-right: 0 !important; padding-left: 0 !important; } - .px-md-1 { padding-right: 0.25rem !important; padding-left: 0.25rem !important; } - .px-md-2 { padding-right: 0.5rem !important; padding-left: 0.5rem !important; } - .px-md-3 { padding-right: 1rem !important; padding-left: 1rem !important; } - .px-md-4 { padding-right: 1.5rem !important; padding-left: 1.5rem !important; } - .px-md-5 { padding-right: 3rem !important; padding-left: 3rem !important; } - .py-md-0 { padding-top: 0 !important; padding-bottom: 0 !important; } - .py-md-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; } - .py-md-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; } - .py-md-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; } - .py-md-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; } - .py-md-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; } - .pt-md-0 { padding-top: 0 !important; } - .pt-md-1 { padding-top: 0.25rem !important; } - .pt-md-2 { padding-top: 0.5rem !important; } - .pt-md-3 { padding-top: 1rem !important; } - .pt-md-4 { padding-top: 1.5rem !important; } - .pt-md-5 { padding-top: 3rem !important; } - .pe-md-0 { padding-right: 0 !important; } - .pe-md-1 { padding-right: 0.25rem !important; } - .pe-md-2 { padding-right: 0.5rem !important; } - .pe-md-3 { padding-right: 1rem !important; } - .pe-md-4 { padding-right: 1.5rem !important; } - .pe-md-5 { padding-right: 3rem !important; } - .pb-md-0 { padding-bottom: 0 !important; } - .pb-md-1 { padding-bottom: 0.25rem !important; } - .pb-md-2 { padding-bottom: 0.5rem !important; } - .pb-md-3 { padding-bottom: 1rem !important; } - .pb-md-4 { padding-bottom: 1.5rem !important; } - .pb-md-5 { padding-bottom: 3rem !important; } - .ps-md-0 { padding-left: 0 !important; } - .ps-md-1 { padding-left: 0.25rem !important; } - .ps-md-2 { padding-left: 0.5rem !important; } - .ps-md-3 { padding-left: 1rem !important; } - .ps-md-4 { padding-left: 1.5rem !important; } - .ps-md-5 { padding-left: 3rem !important; } - + .gap-md-0 { + gap: 0 !important; + } + .gap-md-1 { + gap: 0.25rem !important; + } + .gap-md-2 { + gap: 0.5rem !important; + } + .gap-md-3 { + gap: 1rem !important; + } + .gap-md-4 { + gap: 1.5rem !important; + } + .gap-md-5 { + gap: 3rem !important; + } + .row-gap-md-0 { + row-gap: 0 !important; + } + .row-gap-md-1 { + row-gap: 0.25rem !important; + } + .row-gap-md-2 { + row-gap: 0.5rem !important; + } + .row-gap-md-3 { + row-gap: 1rem !important; + } + .row-gap-md-4 { + row-gap: 1.5rem !important; + } + .row-gap-md-5 { + row-gap: 3rem !important; + } + .column-gap-md-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-md-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-md-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-md-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-md-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-md-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } .text-md-start { text-align: left !important; } - .text-md-end { text-align: right !important; } - .text-md-center { text-align: center !important; } @@ -9343,649 +10424,559 @@ textarea.form-control-lg { .float-lg-start { float: left !important; } - .float-lg-end { float: right !important; } - .float-lg-none { float: none !important; } - + .object-fit-lg-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-lg-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-lg-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-lg-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-lg-none { + -o-object-fit: none !important; + object-fit: none !important; + } .d-lg-inline { display: inline !important; } - .d-lg-inline-block { display: inline-block !important; } - .d-lg-block { display: block !important; } - .d-lg-grid { display: grid !important; } - + .d-lg-inline-grid { + display: inline-grid !important; + } .d-lg-table { display: table !important; } - .d-lg-table-row { display: table-row !important; } - .d-lg-table-cell { display: table-cell !important; } - .d-lg-flex { display: flex !important; } - .d-lg-inline-flex { display: inline-flex !important; } - .d-lg-none { display: none !important; } - .flex-lg-fill { flex: 1 1 auto !important; } - .flex-lg-row { flex-direction: row !important; } - .flex-lg-column { flex-direction: column !important; } - .flex-lg-row-reverse { flex-direction: row-reverse !important; } - .flex-lg-column-reverse { flex-direction: column-reverse !important; } - .flex-lg-grow-0 { flex-grow: 0 !important; } - .flex-lg-grow-1 { flex-grow: 1 !important; } - .flex-lg-shrink-0 { flex-shrink: 0 !important; } - .flex-lg-shrink-1 { flex-shrink: 1 !important; } - .flex-lg-wrap { flex-wrap: wrap !important; } - .flex-lg-nowrap { flex-wrap: nowrap !important; } - .flex-lg-wrap-reverse { flex-wrap: wrap-reverse !important; } - - .gap-lg-0 { - gap: 0 !important; - } - - .gap-lg-1 { - gap: 0.25rem !important; - } - - .gap-lg-2 { - gap: 0.5rem !important; - } - - .gap-lg-3 { - gap: 1rem !important; - } - - .gap-lg-4 { - gap: 1.5rem !important; - } - - .gap-lg-5 { - gap: 3rem !important; - } - .justify-content-lg-start { justify-content: flex-start !important; } - .justify-content-lg-end { justify-content: flex-end !important; } - .justify-content-lg-center { justify-content: center !important; } - .justify-content-lg-between { justify-content: space-between !important; } - .justify-content-lg-around { justify-content: space-around !important; } - .justify-content-lg-evenly { justify-content: space-evenly !important; } - .align-items-lg-start { align-items: flex-start !important; } - .align-items-lg-end { align-items: flex-end !important; } - .align-items-lg-center { align-items: center !important; } - .align-items-lg-baseline { align-items: baseline !important; } - .align-items-lg-stretch { align-items: stretch !important; } - .align-content-lg-start { align-content: flex-start !important; } - .align-content-lg-end { align-content: flex-end !important; } - .align-content-lg-center { align-content: center !important; } - .align-content-lg-between { align-content: space-between !important; } - .align-content-lg-around { align-content: space-around !important; } - .align-content-lg-stretch { align-content: stretch !important; } - .align-self-lg-auto { align-self: auto !important; } - .align-self-lg-start { align-self: flex-start !important; } - .align-self-lg-end { align-self: flex-end !important; } - .align-self-lg-center { align-self: center !important; } - .align-self-lg-baseline { align-self: baseline !important; } - .align-self-lg-stretch { align-self: stretch !important; } - .order-lg-first { order: -1 !important; } - .order-lg-0 { order: 0 !important; } - .order-lg-1 { order: 1 !important; } - .order-lg-2 { order: 2 !important; } - .order-lg-3 { order: 3 !important; } - .order-lg-4 { order: 4 !important; } - .order-lg-5 { order: 5 !important; } - .order-lg-last { order: 6 !important; } - .m-lg-0 { margin: 0 !important; } - .m-lg-1 { margin: 0.25rem !important; } - .m-lg-2 { margin: 0.5rem !important; } - .m-lg-3 { margin: 1rem !important; } - .m-lg-4 { margin: 1.5rem !important; } - .m-lg-5 { margin: 3rem !important; } - .m-lg-auto { margin: auto !important; } - .mx-lg-0 { margin-right: 0 !important; margin-left: 0 !important; } - .mx-lg-1 { margin-right: 0.25rem !important; margin-left: 0.25rem !important; } - .mx-lg-2 { margin-right: 0.5rem !important; margin-left: 0.5rem !important; } - .mx-lg-3 { margin-right: 1rem !important; margin-left: 1rem !important; } - .mx-lg-4 { margin-right: 1.5rem !important; margin-left: 1.5rem !important; } - .mx-lg-5 { margin-right: 3rem !important; margin-left: 3rem !important; } - .mx-lg-auto { margin-right: auto !important; margin-left: auto !important; } - .my-lg-0 { margin-top: 0 !important; margin-bottom: 0 !important; } - .my-lg-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; } - .my-lg-2 { margin-top: 0.5rem !important; margin-bottom: 0.5rem !important; } - .my-lg-3 { margin-top: 1rem !important; margin-bottom: 1rem !important; } - .my-lg-4 { margin-top: 1.5rem !important; margin-bottom: 1.5rem !important; } - .my-lg-5 { margin-top: 3rem !important; margin-bottom: 3rem !important; } - .my-lg-auto { margin-top: auto !important; margin-bottom: auto !important; } - .mt-lg-0 { margin-top: 0 !important; } - .mt-lg-1 { margin-top: 0.25rem !important; } - .mt-lg-2 { margin-top: 0.5rem !important; } - .mt-lg-3 { margin-top: 1rem !important; } - .mt-lg-4 { margin-top: 1.5rem !important; } - .mt-lg-5 { margin-top: 3rem !important; } - .mt-lg-auto { margin-top: auto !important; } - .me-lg-0 { margin-right: 0 !important; } - .me-lg-1 { margin-right: 0.25rem !important; } - .me-lg-2 { margin-right: 0.5rem !important; } - .me-lg-3 { margin-right: 1rem !important; } - .me-lg-4 { margin-right: 1.5rem !important; } - .me-lg-5 { margin-right: 3rem !important; } - .me-lg-auto { margin-right: auto !important; } - .mb-lg-0 { margin-bottom: 0 !important; } - .mb-lg-1 { margin-bottom: 0.25rem !important; } - .mb-lg-2 { margin-bottom: 0.5rem !important; } - .mb-lg-3 { margin-bottom: 1rem !important; } - .mb-lg-4 { margin-bottom: 1.5rem !important; } - .mb-lg-5 { margin-bottom: 3rem !important; } - .mb-lg-auto { margin-bottom: auto !important; } - .ms-lg-0 { margin-left: 0 !important; } - .ms-lg-1 { margin-left: 0.25rem !important; } - .ms-lg-2 { margin-left: 0.5rem !important; } - .ms-lg-3 { margin-left: 1rem !important; } - .ms-lg-4 { margin-left: 1.5rem !important; } - .ms-lg-5 { margin-left: 3rem !important; } - .ms-lg-auto { margin-left: auto !important; } - .p-lg-0 { padding: 0 !important; } - .p-lg-1 { padding: 0.25rem !important; } - .p-lg-2 { padding: 0.5rem !important; } - .p-lg-3 { padding: 1rem !important; } - .p-lg-4 { padding: 1.5rem !important; } - .p-lg-5 { padding: 3rem !important; } - .px-lg-0 { padding-right: 0 !important; padding-left: 0 !important; } - .px-lg-1 { padding-right: 0.25rem !important; padding-left: 0.25rem !important; } - .px-lg-2 { padding-right: 0.5rem !important; padding-left: 0.5rem !important; } - .px-lg-3 { padding-right: 1rem !important; padding-left: 1rem !important; } - .px-lg-4 { padding-right: 1.5rem !important; padding-left: 1.5rem !important; } - .px-lg-5 { padding-right: 3rem !important; padding-left: 3rem !important; } - .py-lg-0 { padding-top: 0 !important; padding-bottom: 0 !important; } - .py-lg-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; } - .py-lg-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; } - .py-lg-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; } - .py-lg-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; } - .py-lg-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; } - .pt-lg-0 { padding-top: 0 !important; } - .pt-lg-1 { padding-top: 0.25rem !important; } - .pt-lg-2 { padding-top: 0.5rem !important; } - .pt-lg-3 { padding-top: 1rem !important; } - .pt-lg-4 { padding-top: 1.5rem !important; } - .pt-lg-5 { padding-top: 3rem !important; } - .pe-lg-0 { padding-right: 0 !important; } - .pe-lg-1 { padding-right: 0.25rem !important; } - .pe-lg-2 { padding-right: 0.5rem !important; } - .pe-lg-3 { padding-right: 1rem !important; } - .pe-lg-4 { padding-right: 1.5rem !important; } - .pe-lg-5 { padding-right: 3rem !important; } - .pb-lg-0 { padding-bottom: 0 !important; } - .pb-lg-1 { padding-bottom: 0.25rem !important; } - .pb-lg-2 { padding-bottom: 0.5rem !important; } - .pb-lg-3 { padding-bottom: 1rem !important; } - .pb-lg-4 { padding-bottom: 1.5rem !important; } - .pb-lg-5 { padding-bottom: 3rem !important; } - .ps-lg-0 { padding-left: 0 !important; } - .ps-lg-1 { padding-left: 0.25rem !important; } - .ps-lg-2 { padding-left: 0.5rem !important; } - .ps-lg-3 { padding-left: 1rem !important; } - .ps-lg-4 { padding-left: 1.5rem !important; } - .ps-lg-5 { padding-left: 3rem !important; } - + .gap-lg-0 { + gap: 0 !important; + } + .gap-lg-1 { + gap: 0.25rem !important; + } + .gap-lg-2 { + gap: 0.5rem !important; + } + .gap-lg-3 { + gap: 1rem !important; + } + .gap-lg-4 { + gap: 1.5rem !important; + } + .gap-lg-5 { + gap: 3rem !important; + } + .row-gap-lg-0 { + row-gap: 0 !important; + } + .row-gap-lg-1 { + row-gap: 0.25rem !important; + } + .row-gap-lg-2 { + row-gap: 0.5rem !important; + } + .row-gap-lg-3 { + row-gap: 1rem !important; + } + .row-gap-lg-4 { + row-gap: 1.5rem !important; + } + .row-gap-lg-5 { + row-gap: 3rem !important; + } + .column-gap-lg-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-lg-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-lg-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-lg-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-lg-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-lg-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } .text-lg-start { text-align: left !important; } - .text-lg-end { text-align: right !important; } - .text-lg-center { text-align: center !important; } @@ -9994,649 +10985,559 @@ textarea.form-control-lg { .float-xl-start { float: left !important; } - .float-xl-end { float: right !important; } - .float-xl-none { float: none !important; } - + .object-fit-xl-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-xl-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-xl-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-xl-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-xl-none { + -o-object-fit: none !important; + object-fit: none !important; + } .d-xl-inline { display: inline !important; } - .d-xl-inline-block { display: inline-block !important; } - .d-xl-block { display: block !important; } - .d-xl-grid { display: grid !important; } - + .d-xl-inline-grid { + display: inline-grid !important; + } .d-xl-table { display: table !important; } - .d-xl-table-row { display: table-row !important; } - .d-xl-table-cell { display: table-cell !important; } - .d-xl-flex { display: flex !important; } - .d-xl-inline-flex { display: inline-flex !important; } - .d-xl-none { display: none !important; } - .flex-xl-fill { flex: 1 1 auto !important; } - .flex-xl-row { flex-direction: row !important; } - .flex-xl-column { flex-direction: column !important; } - .flex-xl-row-reverse { flex-direction: row-reverse !important; } - .flex-xl-column-reverse { flex-direction: column-reverse !important; } - .flex-xl-grow-0 { flex-grow: 0 !important; } - .flex-xl-grow-1 { flex-grow: 1 !important; } - .flex-xl-shrink-0 { flex-shrink: 0 !important; } - .flex-xl-shrink-1 { flex-shrink: 1 !important; } - .flex-xl-wrap { flex-wrap: wrap !important; } - .flex-xl-nowrap { flex-wrap: nowrap !important; } - .flex-xl-wrap-reverse { flex-wrap: wrap-reverse !important; } - - .gap-xl-0 { - gap: 0 !important; - } - - .gap-xl-1 { - gap: 0.25rem !important; - } - - .gap-xl-2 { - gap: 0.5rem !important; - } - - .gap-xl-3 { - gap: 1rem !important; - } - - .gap-xl-4 { - gap: 1.5rem !important; - } - - .gap-xl-5 { - gap: 3rem !important; - } - .justify-content-xl-start { justify-content: flex-start !important; } - .justify-content-xl-end { justify-content: flex-end !important; } - .justify-content-xl-center { justify-content: center !important; } - .justify-content-xl-between { justify-content: space-between !important; } - .justify-content-xl-around { justify-content: space-around !important; } - .justify-content-xl-evenly { justify-content: space-evenly !important; } - .align-items-xl-start { align-items: flex-start !important; } - .align-items-xl-end { align-items: flex-end !important; } - .align-items-xl-center { align-items: center !important; } - .align-items-xl-baseline { align-items: baseline !important; } - .align-items-xl-stretch { align-items: stretch !important; } - .align-content-xl-start { align-content: flex-start !important; } - .align-content-xl-end { align-content: flex-end !important; } - .align-content-xl-center { align-content: center !important; } - .align-content-xl-between { align-content: space-between !important; } - .align-content-xl-around { align-content: space-around !important; } - .align-content-xl-stretch { align-content: stretch !important; } - .align-self-xl-auto { align-self: auto !important; } - .align-self-xl-start { align-self: flex-start !important; } - .align-self-xl-end { align-self: flex-end !important; } - .align-self-xl-center { align-self: center !important; } - .align-self-xl-baseline { align-self: baseline !important; } - .align-self-xl-stretch { align-self: stretch !important; } - .order-xl-first { order: -1 !important; } - .order-xl-0 { order: 0 !important; } - .order-xl-1 { order: 1 !important; } - .order-xl-2 { order: 2 !important; } - .order-xl-3 { order: 3 !important; } - .order-xl-4 { order: 4 !important; } - .order-xl-5 { order: 5 !important; } - .order-xl-last { order: 6 !important; } - .m-xl-0 { margin: 0 !important; } - .m-xl-1 { margin: 0.25rem !important; } - .m-xl-2 { margin: 0.5rem !important; } - .m-xl-3 { margin: 1rem !important; } - .m-xl-4 { margin: 1.5rem !important; } - .m-xl-5 { margin: 3rem !important; } - .m-xl-auto { margin: auto !important; } - .mx-xl-0 { margin-right: 0 !important; margin-left: 0 !important; } - .mx-xl-1 { margin-right: 0.25rem !important; margin-left: 0.25rem !important; } - .mx-xl-2 { margin-right: 0.5rem !important; margin-left: 0.5rem !important; } - .mx-xl-3 { margin-right: 1rem !important; margin-left: 1rem !important; } - .mx-xl-4 { margin-right: 1.5rem !important; margin-left: 1.5rem !important; } - .mx-xl-5 { margin-right: 3rem !important; margin-left: 3rem !important; } - .mx-xl-auto { margin-right: auto !important; margin-left: auto !important; } - .my-xl-0 { margin-top: 0 !important; margin-bottom: 0 !important; } - .my-xl-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; } - .my-xl-2 { margin-top: 0.5rem !important; margin-bottom: 0.5rem !important; } - .my-xl-3 { margin-top: 1rem !important; margin-bottom: 1rem !important; } - .my-xl-4 { margin-top: 1.5rem !important; margin-bottom: 1.5rem !important; } - .my-xl-5 { margin-top: 3rem !important; margin-bottom: 3rem !important; } - .my-xl-auto { margin-top: auto !important; margin-bottom: auto !important; } - .mt-xl-0 { margin-top: 0 !important; } - .mt-xl-1 { margin-top: 0.25rem !important; } - .mt-xl-2 { margin-top: 0.5rem !important; } - .mt-xl-3 { margin-top: 1rem !important; } - .mt-xl-4 { margin-top: 1.5rem !important; } - .mt-xl-5 { margin-top: 3rem !important; } - .mt-xl-auto { margin-top: auto !important; } - .me-xl-0 { margin-right: 0 !important; } - .me-xl-1 { margin-right: 0.25rem !important; } - .me-xl-2 { margin-right: 0.5rem !important; } - .me-xl-3 { margin-right: 1rem !important; } - .me-xl-4 { margin-right: 1.5rem !important; } - .me-xl-5 { margin-right: 3rem !important; } - .me-xl-auto { margin-right: auto !important; } - .mb-xl-0 { margin-bottom: 0 !important; } - .mb-xl-1 { margin-bottom: 0.25rem !important; } - .mb-xl-2 { margin-bottom: 0.5rem !important; } - .mb-xl-3 { margin-bottom: 1rem !important; } - .mb-xl-4 { margin-bottom: 1.5rem !important; } - .mb-xl-5 { margin-bottom: 3rem !important; } - .mb-xl-auto { margin-bottom: auto !important; } - .ms-xl-0 { margin-left: 0 !important; } - .ms-xl-1 { margin-left: 0.25rem !important; } - .ms-xl-2 { margin-left: 0.5rem !important; } - .ms-xl-3 { margin-left: 1rem !important; } - .ms-xl-4 { margin-left: 1.5rem !important; } - .ms-xl-5 { margin-left: 3rem !important; } - .ms-xl-auto { margin-left: auto !important; } - .p-xl-0 { padding: 0 !important; } - .p-xl-1 { padding: 0.25rem !important; } - .p-xl-2 { padding: 0.5rem !important; } - .p-xl-3 { padding: 1rem !important; } - .p-xl-4 { padding: 1.5rem !important; } - .p-xl-5 { padding: 3rem !important; } - .px-xl-0 { padding-right: 0 !important; padding-left: 0 !important; } - .px-xl-1 { padding-right: 0.25rem !important; padding-left: 0.25rem !important; } - .px-xl-2 { padding-right: 0.5rem !important; padding-left: 0.5rem !important; } - .px-xl-3 { padding-right: 1rem !important; padding-left: 1rem !important; } - .px-xl-4 { padding-right: 1.5rem !important; padding-left: 1.5rem !important; } - .px-xl-5 { padding-right: 3rem !important; padding-left: 3rem !important; } - .py-xl-0 { padding-top: 0 !important; padding-bottom: 0 !important; } - .py-xl-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; } - .py-xl-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; } - .py-xl-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; } - .py-xl-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; } - .py-xl-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; } - .pt-xl-0 { padding-top: 0 !important; } - .pt-xl-1 { padding-top: 0.25rem !important; } - .pt-xl-2 { padding-top: 0.5rem !important; } - .pt-xl-3 { padding-top: 1rem !important; } - .pt-xl-4 { padding-top: 1.5rem !important; } - .pt-xl-5 { padding-top: 3rem !important; } - .pe-xl-0 { padding-right: 0 !important; } - .pe-xl-1 { padding-right: 0.25rem !important; } - .pe-xl-2 { padding-right: 0.5rem !important; } - .pe-xl-3 { padding-right: 1rem !important; } - .pe-xl-4 { padding-right: 1.5rem !important; } - .pe-xl-5 { padding-right: 3rem !important; } - .pb-xl-0 { padding-bottom: 0 !important; } - .pb-xl-1 { padding-bottom: 0.25rem !important; } - .pb-xl-2 { padding-bottom: 0.5rem !important; } - .pb-xl-3 { padding-bottom: 1rem !important; } - .pb-xl-4 { padding-bottom: 1.5rem !important; } - .pb-xl-5 { padding-bottom: 3rem !important; } - .ps-xl-0 { padding-left: 0 !important; } - .ps-xl-1 { padding-left: 0.25rem !important; } - .ps-xl-2 { padding-left: 0.5rem !important; } - .ps-xl-3 { padding-left: 1rem !important; } - .ps-xl-4 { padding-left: 1.5rem !important; } - .ps-xl-5 { padding-left: 3rem !important; } - + .gap-xl-0 { + gap: 0 !important; + } + .gap-xl-1 { + gap: 0.25rem !important; + } + .gap-xl-2 { + gap: 0.5rem !important; + } + .gap-xl-3 { + gap: 1rem !important; + } + .gap-xl-4 { + gap: 1.5rem !important; + } + .gap-xl-5 { + gap: 3rem !important; + } + .row-gap-xl-0 { + row-gap: 0 !important; + } + .row-gap-xl-1 { + row-gap: 0.25rem !important; + } + .row-gap-xl-2 { + row-gap: 0.5rem !important; + } + .row-gap-xl-3 { + row-gap: 1rem !important; + } + .row-gap-xl-4 { + row-gap: 1.5rem !important; + } + .row-gap-xl-5 { + row-gap: 3rem !important; + } + .column-gap-xl-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-xl-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-xl-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-xl-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-xl-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-xl-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } .text-xl-start { text-align: left !important; } - .text-xl-end { text-align: right !important; } - .text-xl-center { text-align: center !important; } @@ -10645,649 +11546,559 @@ textarea.form-control-lg { .float-xxl-start { float: left !important; } - .float-xxl-end { float: right !important; } - .float-xxl-none { float: none !important; } - + .object-fit-xxl-contain { + -o-object-fit: contain !important; + object-fit: contain !important; + } + .object-fit-xxl-cover { + -o-object-fit: cover !important; + object-fit: cover !important; + } + .object-fit-xxl-fill { + -o-object-fit: fill !important; + object-fit: fill !important; + } + .object-fit-xxl-scale { + -o-object-fit: scale-down !important; + object-fit: scale-down !important; + } + .object-fit-xxl-none { + -o-object-fit: none !important; + object-fit: none !important; + } .d-xxl-inline { display: inline !important; } - .d-xxl-inline-block { display: inline-block !important; } - .d-xxl-block { display: block !important; } - .d-xxl-grid { display: grid !important; } - + .d-xxl-inline-grid { + display: inline-grid !important; + } .d-xxl-table { display: table !important; } - .d-xxl-table-row { display: table-row !important; } - .d-xxl-table-cell { display: table-cell !important; } - .d-xxl-flex { display: flex !important; } - .d-xxl-inline-flex { display: inline-flex !important; } - .d-xxl-none { display: none !important; } - .flex-xxl-fill { flex: 1 1 auto !important; } - .flex-xxl-row { flex-direction: row !important; } - .flex-xxl-column { flex-direction: column !important; } - .flex-xxl-row-reverse { flex-direction: row-reverse !important; } - .flex-xxl-column-reverse { flex-direction: column-reverse !important; } - .flex-xxl-grow-0 { flex-grow: 0 !important; } - .flex-xxl-grow-1 { flex-grow: 1 !important; } - .flex-xxl-shrink-0 { flex-shrink: 0 !important; } - .flex-xxl-shrink-1 { flex-shrink: 1 !important; } - .flex-xxl-wrap { flex-wrap: wrap !important; } - .flex-xxl-nowrap { flex-wrap: nowrap !important; } - .flex-xxl-wrap-reverse { flex-wrap: wrap-reverse !important; } - - .gap-xxl-0 { - gap: 0 !important; - } - - .gap-xxl-1 { - gap: 0.25rem !important; - } - - .gap-xxl-2 { - gap: 0.5rem !important; - } - - .gap-xxl-3 { - gap: 1rem !important; - } - - .gap-xxl-4 { - gap: 1.5rem !important; - } - - .gap-xxl-5 { - gap: 3rem !important; - } - .justify-content-xxl-start { justify-content: flex-start !important; } - .justify-content-xxl-end { justify-content: flex-end !important; } - .justify-content-xxl-center { justify-content: center !important; } - .justify-content-xxl-between { justify-content: space-between !important; } - .justify-content-xxl-around { justify-content: space-around !important; } - .justify-content-xxl-evenly { justify-content: space-evenly !important; } - .align-items-xxl-start { align-items: flex-start !important; } - .align-items-xxl-end { align-items: flex-end !important; } - .align-items-xxl-center { align-items: center !important; } - .align-items-xxl-baseline { align-items: baseline !important; } - .align-items-xxl-stretch { align-items: stretch !important; } - .align-content-xxl-start { align-content: flex-start !important; } - .align-content-xxl-end { align-content: flex-end !important; } - .align-content-xxl-center { align-content: center !important; } - .align-content-xxl-between { align-content: space-between !important; } - .align-content-xxl-around { align-content: space-around !important; } - .align-content-xxl-stretch { align-content: stretch !important; } - .align-self-xxl-auto { align-self: auto !important; } - .align-self-xxl-start { align-self: flex-start !important; } - .align-self-xxl-end { align-self: flex-end !important; } - .align-self-xxl-center { align-self: center !important; } - .align-self-xxl-baseline { align-self: baseline !important; } - .align-self-xxl-stretch { align-self: stretch !important; } - .order-xxl-first { order: -1 !important; } - .order-xxl-0 { order: 0 !important; } - .order-xxl-1 { order: 1 !important; } - .order-xxl-2 { order: 2 !important; } - .order-xxl-3 { order: 3 !important; } - .order-xxl-4 { order: 4 !important; } - .order-xxl-5 { order: 5 !important; } - .order-xxl-last { order: 6 !important; } - .m-xxl-0 { margin: 0 !important; } - .m-xxl-1 { margin: 0.25rem !important; } - .m-xxl-2 { margin: 0.5rem !important; } - .m-xxl-3 { margin: 1rem !important; } - .m-xxl-4 { margin: 1.5rem !important; } - .m-xxl-5 { margin: 3rem !important; } - .m-xxl-auto { margin: auto !important; } - .mx-xxl-0 { margin-right: 0 !important; margin-left: 0 !important; } - .mx-xxl-1 { margin-right: 0.25rem !important; margin-left: 0.25rem !important; } - .mx-xxl-2 { margin-right: 0.5rem !important; margin-left: 0.5rem !important; } - .mx-xxl-3 { margin-right: 1rem !important; margin-left: 1rem !important; } - .mx-xxl-4 { margin-right: 1.5rem !important; margin-left: 1.5rem !important; } - .mx-xxl-5 { margin-right: 3rem !important; margin-left: 3rem !important; } - .mx-xxl-auto { margin-right: auto !important; margin-left: auto !important; } - .my-xxl-0 { margin-top: 0 !important; margin-bottom: 0 !important; } - .my-xxl-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; } - .my-xxl-2 { margin-top: 0.5rem !important; margin-bottom: 0.5rem !important; } - .my-xxl-3 { margin-top: 1rem !important; margin-bottom: 1rem !important; } - .my-xxl-4 { margin-top: 1.5rem !important; margin-bottom: 1.5rem !important; } - .my-xxl-5 { margin-top: 3rem !important; margin-bottom: 3rem !important; } - .my-xxl-auto { margin-top: auto !important; margin-bottom: auto !important; } - .mt-xxl-0 { margin-top: 0 !important; } - .mt-xxl-1 { margin-top: 0.25rem !important; } - .mt-xxl-2 { margin-top: 0.5rem !important; } - .mt-xxl-3 { margin-top: 1rem !important; } - .mt-xxl-4 { margin-top: 1.5rem !important; } - .mt-xxl-5 { margin-top: 3rem !important; } - .mt-xxl-auto { margin-top: auto !important; } - .me-xxl-0 { margin-right: 0 !important; } - .me-xxl-1 { margin-right: 0.25rem !important; } - .me-xxl-2 { margin-right: 0.5rem !important; } - .me-xxl-3 { margin-right: 1rem !important; } - .me-xxl-4 { margin-right: 1.5rem !important; } - .me-xxl-5 { margin-right: 3rem !important; } - .me-xxl-auto { margin-right: auto !important; } - .mb-xxl-0 { margin-bottom: 0 !important; } - .mb-xxl-1 { margin-bottom: 0.25rem !important; } - .mb-xxl-2 { margin-bottom: 0.5rem !important; } - .mb-xxl-3 { margin-bottom: 1rem !important; } - .mb-xxl-4 { margin-bottom: 1.5rem !important; } - .mb-xxl-5 { margin-bottom: 3rem !important; } - .mb-xxl-auto { margin-bottom: auto !important; } - .ms-xxl-0 { margin-left: 0 !important; } - .ms-xxl-1 { margin-left: 0.25rem !important; } - .ms-xxl-2 { margin-left: 0.5rem !important; } - .ms-xxl-3 { margin-left: 1rem !important; } - .ms-xxl-4 { margin-left: 1.5rem !important; } - .ms-xxl-5 { margin-left: 3rem !important; } - .ms-xxl-auto { margin-left: auto !important; } - .p-xxl-0 { padding: 0 !important; } - .p-xxl-1 { padding: 0.25rem !important; } - .p-xxl-2 { padding: 0.5rem !important; } - .p-xxl-3 { padding: 1rem !important; } - .p-xxl-4 { padding: 1.5rem !important; } - .p-xxl-5 { padding: 3rem !important; } - .px-xxl-0 { padding-right: 0 !important; padding-left: 0 !important; } - .px-xxl-1 { padding-right: 0.25rem !important; padding-left: 0.25rem !important; } - .px-xxl-2 { padding-right: 0.5rem !important; padding-left: 0.5rem !important; } - .px-xxl-3 { padding-right: 1rem !important; padding-left: 1rem !important; } - .px-xxl-4 { padding-right: 1.5rem !important; padding-left: 1.5rem !important; } - .px-xxl-5 { padding-right: 3rem !important; padding-left: 3rem !important; } - .py-xxl-0 { padding-top: 0 !important; padding-bottom: 0 !important; } - .py-xxl-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; } - .py-xxl-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; } - .py-xxl-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; } - .py-xxl-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; } - .py-xxl-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; } - .pt-xxl-0 { padding-top: 0 !important; } - .pt-xxl-1 { padding-top: 0.25rem !important; } - .pt-xxl-2 { padding-top: 0.5rem !important; } - .pt-xxl-3 { padding-top: 1rem !important; } - .pt-xxl-4 { padding-top: 1.5rem !important; } - .pt-xxl-5 { padding-top: 3rem !important; } - .pe-xxl-0 { padding-right: 0 !important; } - .pe-xxl-1 { padding-right: 0.25rem !important; } - .pe-xxl-2 { padding-right: 0.5rem !important; } - .pe-xxl-3 { padding-right: 1rem !important; } - .pe-xxl-4 { padding-right: 1.5rem !important; } - .pe-xxl-5 { padding-right: 3rem !important; } - .pb-xxl-0 { padding-bottom: 0 !important; } - .pb-xxl-1 { padding-bottom: 0.25rem !important; } - .pb-xxl-2 { padding-bottom: 0.5rem !important; } - .pb-xxl-3 { padding-bottom: 1rem !important; } - .pb-xxl-4 { padding-bottom: 1.5rem !important; } - .pb-xxl-5 { padding-bottom: 3rem !important; } - .ps-xxl-0 { padding-left: 0 !important; } - .ps-xxl-1 { padding-left: 0.25rem !important; } - .ps-xxl-2 { padding-left: 0.5rem !important; } - .ps-xxl-3 { padding-left: 1rem !important; } - .ps-xxl-4 { padding-left: 1.5rem !important; } - .ps-xxl-5 { padding-left: 3rem !important; } - + .gap-xxl-0 { + gap: 0 !important; + } + .gap-xxl-1 { + gap: 0.25rem !important; + } + .gap-xxl-2 { + gap: 0.5rem !important; + } + .gap-xxl-3 { + gap: 1rem !important; + } + .gap-xxl-4 { + gap: 1.5rem !important; + } + .gap-xxl-5 { + gap: 3rem !important; + } + .row-gap-xxl-0 { + row-gap: 0 !important; + } + .row-gap-xxl-1 { + row-gap: 0.25rem !important; + } + .row-gap-xxl-2 { + row-gap: 0.5rem !important; + } + .row-gap-xxl-3 { + row-gap: 1rem !important; + } + .row-gap-xxl-4 { + row-gap: 1.5rem !important; + } + .row-gap-xxl-5 { + row-gap: 3rem !important; + } + .column-gap-xxl-0 { + -moz-column-gap: 0 !important; + column-gap: 0 !important; + } + .column-gap-xxl-1 { + -moz-column-gap: 0.25rem !important; + column-gap: 0.25rem !important; + } + .column-gap-xxl-2 { + -moz-column-gap: 0.5rem !important; + column-gap: 0.5rem !important; + } + .column-gap-xxl-3 { + -moz-column-gap: 1rem !important; + column-gap: 1rem !important; + } + .column-gap-xxl-4 { + -moz-column-gap: 1.5rem !important; + column-gap: 1.5rem !important; + } + .column-gap-xxl-5 { + -moz-column-gap: 3rem !important; + column-gap: 3rem !important; + } .text-xxl-start { text-align: left !important; } - .text-xxl-end { text-align: right !important; } - .text-xxl-center { text-align: center !important; } @@ -11296,15 +12107,12 @@ textarea.form-control-lg { .fs-1 { font-size: 2.5rem !important; } - .fs-2 { font-size: 2rem !important; } - .fs-3 { font-size: 1.75rem !important; } - .fs-4 { font-size: 1.5rem !important; } @@ -11313,39 +12121,33 @@ textarea.form-control-lg { .d-print-inline { display: inline !important; } - .d-print-inline-block { display: inline-block !important; } - .d-print-block { display: block !important; } - .d-print-grid { display: grid !important; } - + .d-print-inline-grid { + display: inline-grid !important; + } .d-print-table { display: table !important; } - .d-print-table-row { display: table-row !important; } - .d-print-table-cell { display: table-cell !important; } - .d-print-flex { display: flex !important; } - .d-print-inline-flex { display: inline-flex !important; } - .d-print-none { display: none !important; } @@ -11378,6 +12180,9 @@ textarea.form-control-lg { .navbar.bg-dark { border-color: #4d4d4d; } +.navbar.bg-body-secondary, .navbar.bg-body-tertiary { + border-color: rgba(0, 0, 0, 0.1); +} .btn { border-style: solid; @@ -11441,6 +12246,10 @@ textarea.form-control-lg { border-color: #4d4d4d; } +.btn-outline-secondary { + color: var(--bs-tertiary-color); +} + [class*=btn-outline] { border-top-width: 1px; } @@ -11462,6 +12271,11 @@ textarea.form-control-lg { color: #999; } +.table-secondary, +.table-light { + --bs-table-color: #222; +} + .form-control { box-shadow: inset 0 2px 0 rgba(0, 0, 0, 0.075); } @@ -11473,16 +12287,17 @@ textarea.form-control-lg { } .nav-tabs .nav-link { - color: #222; + color: var(--bs-body-color); } .nav-tabs .nav-link, .nav-tabs .nav-link.disabled, .nav-tabs .nav-link.disabled:hover, .nav-tabs .nav-link.disabled:focus { margin-top: 6px; - border-color: #f0f0f0; + border-color: var(--bs-border-color); transition: padding-bottom 0.2s ease-in-out, margin-top 0.2s ease-in-out, border-bottom 0.2s ease-in-out; } .nav-tabs .nav-link:not(.disabled):hover, .nav-tabs .nav-link:not(.disabled):focus, .nav-tabs .nav-link.active { padding-bottom: calc(0.5rem + 6px); margin-top: 0; + color: var(--bs-body-color); border-bottom-color: transparent; } .nav-tabs.nav-justified > li { @@ -11497,7 +12312,7 @@ textarea.form-control-lg { } .breadcrumb { - border-color: #d8d8d8; + border-color: var(--bs-border-color); border-style: solid; border-width: 0 1px 4px 1px; } diff --git a/data/web/css/themes/mailcow-darkmode.css b/data/web/css/themes/mailcow-darkmode.css index 83befeaf5..901a6bcc6 100644 --- a/data/web/css/themes/mailcow-darkmode.css +++ b/data/web/css/themes/mailcow-darkmode.css @@ -1,62 +1,103 @@ +/* theme tokens, override in css/themes//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 { diff --git a/data/web/css/themes/servercow/dark.css b/data/web/css/themes/servercow/dark.css new file mode 100644 index 000000000..569435094 --- /dev/null +++ b/data/web/css/themes/servercow/dark.css @@ -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); +} diff --git a/data/web/css/themes/servercow/fonts/LICENSE-RobotoSlab.txt b/data/web/css/themes/servercow/fonts/LICENSE-RobotoSlab.txt new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/data/web/css/themes/servercow/fonts/LICENSE-RobotoSlab.txt @@ -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. diff --git a/data/web/css/themes/servercow/fonts/OFL-Poppins.txt b/data/web/css/themes/servercow/fonts/OFL-Poppins.txt new file mode 100644 index 000000000..76df3b565 --- /dev/null +++ b/data/web/css/themes/servercow/fonts/OFL-Poppins.txt @@ -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. diff --git a/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff new file mode 100644 index 000000000..fc6d5c763 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff differ diff --git a/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff2 b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff2 new file mode 100644 index 000000000..abfe99137 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-500.woff2 differ diff --git a/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff new file mode 100644 index 000000000..08368fc22 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff differ diff --git a/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff2 b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff2 new file mode 100644 index 000000000..068762f85 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-600.woff2 differ diff --git a/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff new file mode 100644 index 000000000..86fbaddf5 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff differ diff --git a/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff2 b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff2 new file mode 100644 index 000000000..981bdb300 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-italic.woff2 differ diff --git a/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff new file mode 100644 index 000000000..b925aa32e Binary files /dev/null and b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff differ diff --git a/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff2 b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff2 new file mode 100644 index 000000000..d83f92c09 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/poppins-v24-latin_latin-ext-regular.woff2 differ diff --git a/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff b/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff new file mode 100644 index 000000000..8621fd61e Binary files /dev/null and b/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff differ diff --git a/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff2 b/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff2 new file mode 100644 index 000000000..2e4097322 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-500.woff2 differ diff --git a/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff b/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff new file mode 100644 index 000000000..8c8b94f48 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff differ diff --git a/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff2 b/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff2 new file mode 100644 index 000000000..f55bf3760 Binary files /dev/null and b/data/web/css/themes/servercow/fonts/roboto-slab-v36-cyrillic_greek_latin-regular.woff2 differ diff --git a/data/web/css/themes/servercow/theme.css b/data/web/css/themes/servercow/theme.css new file mode 100644 index 000000000..14bcecd1a --- /dev/null +++ b/data/web/css/themes/servercow/theme.css @@ -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); +} diff --git a/data/web/css/themes/servercow/theme.json b/data/web/css/themes/servercow/theme.json new file mode 100644 index 000000000..2c8f041bb --- /dev/null +++ b/data/web/css/themes/servercow/theme.json @@ -0,0 +1,4 @@ +{ + "name": "servercow", + "description": "Based on the servercow theme" +} diff --git a/data/web/domainadmin/index.php b/data/web/domainadmin/index.php index 0d70ec3ae..07c62a7d2 100644 --- a/data/web/domainadmin/index.php +++ b/data/web/domainadmin/index.php @@ -3,8 +3,11 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); + // Only redirect to mailbox if NO pending actions + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + header('Location: /domainadmin/mailbox'); + exit(); + } } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { header('Location: /admin/dashboard'); diff --git a/data/web/domainadmin/mailbox.php b/data/web/domainadmin/mailbox.php index bb2ef16f3..8beb5abb9 100644 --- a/data/web/domainadmin/mailbox.php +++ b/data/web/domainadmin/mailbox.php @@ -2,18 +2,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin/dashboard'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != "domainadmin") { - header('Location: /domainadmin'); - exit(); -} +protect_route(['domainadmin']); require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; diff --git a/data/web/domainadmin/user.php b/data/web/domainadmin/user.php index 7f1b392e0..a45e00dfc 100644 --- a/data/web/domainadmin/user.php +++ b/data/web/domainadmin/user.php @@ -2,41 +2,28 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/triggers.domainadmin.inc.php'; -if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { +/* +/ DOMAIN ADMIN +*/ - /* - / DOMAIN ADMIN - */ +protect_route(['domainadmin']); - require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; - $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; - $tfa_data = get_tfa(); - $fido2_data = fido2(array("action" => "get_friendly_names")); - $username = $_SESSION['mailcow_cc_username']; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; +$_SESSION['return_to'] = $_SERVER['REQUEST_URI']; +$tfa_data = get_tfa(); +$fido2_data = fido2(array("action" => "get_friendly_names")); +$username = $_SESSION['mailcow_cc_username']; - $template = 'domainadmin.twig'; - $template_data = [ - 'acl' => $_SESSION['acl'], - 'acl_json' => json_encode($_SESSION['acl']), - 'user_spam_score' => mailbox('get', 'spam_score', $username), - 'tfa_data' => $tfa_data, - 'fido2_data' => $fido2_data, - 'lang_user' => json_encode($lang['user']), - 'lang_datatables' => json_encode($lang['datatables']), - ]; -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin/dashboard'); - exit(); -} -elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - header('Location: /user'); - exit(); -} -else { - header('Location: /domainadmin'); - exit(); -} +$template = 'domainadmin.twig'; +$template_data = [ + 'acl' => $_SESSION['acl'], + 'acl_json' => json_encode($_SESSION['acl']), + 'user_spam_score' => mailbox('get', 'spam_score', $username), + 'tfa_data' => $tfa_data, + 'fido2_data' => $fido2_data, + 'lang_user' => json_encode($lang['user']), + 'lang_datatables' => json_encode($lang['datatables']), +]; $js_minifier->add('/web/js/site/user.js'); $js_minifier->add('/web/js/site/pwgen.js'); diff --git a/data/web/edit.php b/data/web/edit.php index 57cf24bd2..48f2309c1 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -1,10 +1,8 @@ ' . $current[$data_field[$current['type']]]; + $state .= '
' . 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 .= '
' . $current[$data_field[$current['type']]] . state_optional; + $state .= '
' . 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('
', $state); } @@ -436,12 +436,22 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm %s %s %s - ', $record[0], $record[1], $record[2], $state); + ', htmlspecialchars($record[0]), htmlspecialchars($record[1]), $record[2], $state); $record[3] = explode('
', $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; } } } ?> - .txt' type='text/csv'>Download + .txt' type='text/csv'>Download "); - $this->initialized = true; - } - - $title = '-'; - if (isset($context['request'])) { - $request = $context['request']; - $controller = "{$this->dumper->dump($request['controller'], true, ['maxDepth' => 0])}"; - $title = sprintf('%s %s', $request['method'], $uri = $request['uri'], $uri); - $dedupIdentifier = $request['identifier']; - } elseif (isset($context['cli'])) { - $title = '$ '.$context['cli']['command_line']; - $dedupIdentifier = $context['cli']['identifier']; - } else { - $dedupIdentifier = uniqid('', true); - } - - $sourceDescription = ''; - if (isset($context['source'])) { - $source = $context['source']; - $projectDir = $source['project_dir'] ?? null; - $sourceDescription = sprintf('%s on line %d', $source['name'], $source['line']); - if (isset($source['file_link'])) { - $sourceDescription = sprintf('%s', $source['file_link'], $sourceDescription); - } - } - - $isoDate = $this->extractDate($context, 'c'); - $tags = array_filter([ - 'controller' => $controller ?? null, - 'project dir' => $projectDir ?? null, - ]); - - $output->writeln(<< -
-
-

$title

- -
- {$this->renderTags($tags)} -
-
-

- $sourceDescription -

- {$this->dumper->dump($data, true)} -
- -HTML - ); - } - - private function extractDate(array $context, string $format = 'r'): string - { - return date($format, (int) $context['timestamp']); - } - - private function renderTags(array $tags): string - { - if (!$tags) { - return ''; - } - - $renderedTags = ''; - foreach ($tags as $key => $value) { - $renderedTags .= sprintf('
  • %s%s
  • ', $key, $value); - } - - return << -
      - $renderedTags -
    - -HTML; - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Command/ServerDumpCommand.php b/data/web/inc/lib/vendor/symfony/var-dumper/Command/ServerDumpCommand.php deleted file mode 100644 index b64a884b9..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Command/ServerDumpCommand.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Command; - -use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Completion\CompletionInput; -use Symfony\Component\Console\Completion\CompletionSuggestions; -use Symfony\Component\Console\Exception\InvalidArgumentException; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\VarDumper\Cloner\Data; -use Symfony\Component\VarDumper\Command\Descriptor\CliDescriptor; -use Symfony\Component\VarDumper\Command\Descriptor\DumpDescriptorInterface; -use Symfony\Component\VarDumper\Command\Descriptor\HtmlDescriptor; -use Symfony\Component\VarDumper\Dumper\CliDumper; -use Symfony\Component\VarDumper\Dumper\HtmlDumper; -use Symfony\Component\VarDumper\Server\DumpServer; - -/** - * Starts a dump server to collect and output dumps on a single place with multiple formats support. - * - * @author Maxime Steinhausser - * - * @final - */ -#[AsCommand(name: 'server:dump', description: 'Start a dump server that collects and displays dumps in a single place')] -class ServerDumpCommand extends Command -{ - private DumpServer $server; - - /** @var DumpDescriptorInterface[] */ - private array $descriptors; - - public function __construct(DumpServer $server, array $descriptors = []) - { - $this->server = $server; - $this->descriptors = $descriptors + [ - 'cli' => new CliDescriptor(new CliDumper()), - 'html' => new HtmlDescriptor(new HtmlDumper()), - ]; - - parent::__construct(); - } - - protected function configure(): void - { - $this - ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format (%s)', implode(', ', $this->getAvailableFormats())), 'cli') - ->setHelp(<<<'EOF' -%command.name% starts a dump server that collects and displays -dumps in a single place for debugging you application: - - php %command.full_name% - -You can consult dumped data in HTML format in your browser by providing the --format=html option -and redirecting the output to a file: - - php %command.full_name% --format="html" > dump.html - -EOF - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $io = new SymfonyStyle($input, $output); - $format = $input->getOption('format'); - - if (!$descriptor = $this->descriptors[$format] ?? null) { - throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $format)); - } - - $errorIo = $io->getErrorStyle(); - $errorIo->title('Symfony Var Dumper Server'); - - $this->server->start(); - - $errorIo->success(sprintf('Server listening on %s', $this->server->getHost())); - $errorIo->comment('Quit the server with CONTROL-C.'); - - $this->server->listen(function (Data $data, array $context, int $clientId) use ($descriptor, $io) { - $descriptor->describe($io, $data, $context, $clientId); - }); - - return 0; - } - - public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void - { - if ($input->mustSuggestOptionValuesFor('format')) { - $suggestions->suggestValues($this->getAvailableFormats()); - } - } - - private function getAvailableFormats(): array - { - return array_keys($this->descriptors); - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/AbstractDumper.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/AbstractDumper.php deleted file mode 100644 index 53165ba64..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/AbstractDumper.php +++ /dev/null @@ -1,204 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper; - -use Symfony\Component\VarDumper\Cloner\Data; -use Symfony\Component\VarDumper\Cloner\DumperInterface; - -/** - * Abstract mechanism for dumping a Data object. - * - * @author Nicolas Grekas - */ -abstract class AbstractDumper implements DataDumperInterface, DumperInterface -{ - public const DUMP_LIGHT_ARRAY = 1; - public const DUMP_STRING_LENGTH = 2; - public const DUMP_COMMA_SEPARATOR = 4; - public const DUMP_TRAILING_COMMA = 8; - - /** @var callable|resource|string|null */ - public static $defaultOutput = 'php://output'; - - protected $line = ''; - /** @var callable|null */ - protected $lineDumper; - /** @var resource|null */ - protected $outputStream; - protected $decimalPoint = '.'; - protected $indentPad = ' '; - protected $flags; - - private string $charset = ''; - - /** - * @param callable|resource|string|null $output A line dumper callable, an opened stream or an output path, defaults to static::$defaultOutput - * @param string|null $charset The default character encoding to use for non-UTF8 strings - * @param int $flags A bit field of static::DUMP_* constants to fine tune dumps representation - */ - public function __construct($output = null, ?string $charset = null, int $flags = 0) - { - $this->flags = $flags; - $this->setCharset($charset ?: \ini_get('php.output_encoding') ?: \ini_get('default_charset') ?: 'UTF-8'); - $this->setOutput($output ?: static::$defaultOutput); - if (!$output && \is_string(static::$defaultOutput)) { - static::$defaultOutput = $this->outputStream; - } - } - - /** - * Sets the output destination of the dumps. - * - * @param callable|resource|string|null $output A line dumper callable, an opened stream or an output path - * - * @return callable|resource|string|null The previous output destination - */ - public function setOutput($output) - { - $prev = $this->outputStream ?? $this->lineDumper; - - if (\is_callable($output)) { - $this->outputStream = null; - $this->lineDumper = $output; - } else { - if (\is_string($output)) { - $output = fopen($output, 'w'); - } - $this->outputStream = $output; - $this->lineDumper = $this->echoLine(...); - } - - return $prev; - } - - /** - * Sets the default character encoding to use for non-UTF8 strings. - * - * @return string The previous charset - */ - public function setCharset(string $charset): string - { - $prev = $this->charset; - - $charset = strtoupper($charset); - $charset = null === $charset || 'UTF-8' === $charset || 'UTF8' === $charset ? 'CP1252' : $charset; - - $this->charset = $charset; - - return $prev; - } - - /** - * Sets the indentation pad string. - * - * @param string $pad A string that will be prepended to dumped lines, repeated by nesting level - * - * @return string The previous indent pad - */ - public function setIndentPad(string $pad): string - { - $prev = $this->indentPad; - $this->indentPad = $pad; - - return $prev; - } - - /** - * Dumps a Data object. - * - * @param callable|resource|string|true|null $output A line dumper callable, an opened stream, an output path or true to return the dump - * - * @return string|null The dump as string when $output is true - */ - public function dump(Data $data, $output = null): ?string - { - if ($locale = $this->flags & (self::DUMP_COMMA_SEPARATOR | self::DUMP_TRAILING_COMMA) ? setlocale(\LC_NUMERIC, 0) : null) { - setlocale(\LC_NUMERIC, 'C'); - } - - if ($returnDump = true === $output) { - $output = fopen('php://memory', 'r+'); - } - if ($output) { - $prevOutput = $this->setOutput($output); - } - try { - $data->dump($this); - $this->dumpLine(-1); - - if ($returnDump) { - $result = stream_get_contents($output, -1, 0); - fclose($output); - - return $result; - } - } finally { - if ($output) { - $this->setOutput($prevOutput); - } - if ($locale) { - setlocale(\LC_NUMERIC, $locale); - } - } - - return null; - } - - /** - * Dumps the current line. - * - * @param int $depth The recursive depth in the dumped structure for the line being dumped, - * or -1 to signal the end-of-dump to the line dumper callable - * - * @return void - */ - protected function dumpLine(int $depth) - { - ($this->lineDumper)($this->line, $depth, $this->indentPad); - $this->line = ''; - } - - /** - * Generic line dumper callback. - * - * @return void - */ - protected function echoLine(string $line, int $depth, string $indentPad) - { - if (-1 !== $depth) { - fwrite($this->outputStream, str_repeat($indentPad, $depth).$line."\n"); - } - } - - /** - * Converts a non-UTF-8 string to UTF-8. - */ - protected function utf8Encode(?string $s): ?string - { - if (null === $s || preg_match('//u', $s)) { - return $s; - } - - if (!\function_exists('iconv')) { - throw new \RuntimeException('Unable to convert a non-UTF-8 string to UTF-8: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); - } - - if (false !== $c = @iconv($this->charset, 'UTF-8', $s)) { - return $c; - } - if ('CP1252' !== $this->charset && false !== $c = @iconv('CP1252', 'UTF-8', $s)) { - return $c; - } - - return iconv('CP850', 'UTF-8', $s); - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/CliDumper.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/CliDumper.php deleted file mode 100644 index af9637072..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/CliDumper.php +++ /dev/null @@ -1,677 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper; - -use Symfony\Component\VarDumper\Cloner\Cursor; -use Symfony\Component\VarDumper\Cloner\Stub; - -/** - * CliDumper dumps variables for command line output. - * - * @author Nicolas Grekas - */ -class CliDumper extends AbstractDumper -{ - public static $defaultColors; - /** @var callable|resource|string|null */ - public static $defaultOutput = 'php://stdout'; - - protected $colors; - protected $maxStringWidth = 0; - protected $styles = [ - // See http://en.wikipedia.org/wiki/ANSI_escape_code#graphics - 'default' => '0;38;5;208', - 'num' => '1;38;5;38', - 'const' => '1;38;5;208', - 'str' => '1;38;5;113', - 'note' => '38;5;38', - 'ref' => '38;5;247', - 'public' => '', - 'protected' => '', - 'private' => '', - 'meta' => '38;5;170', - 'key' => '38;5;113', - 'index' => '38;5;38', - ]; - - protected static $controlCharsRx = '/[\x00-\x1F\x7F]+/'; - protected static $controlCharsMap = [ - "\t" => '\t', - "\n" => '\n', - "\v" => '\v', - "\f" => '\f', - "\r" => '\r', - "\033" => '\e', - ]; - protected static $unicodeCharsRx = "/[\u{00A0}\u{00AD}\u{034F}\u{061C}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180E}\u{2000}-\u{200F}\u{202F}\u{205F}\u{2060}-\u{2064}\u{206A}-\u{206F}\u{3000}\u{2800}\u{3164}\u{FEFF}\u{FFA0}\u{1D159}\u{1D173}-\u{1D17A}]/u"; - - protected $collapseNextHash = false; - protected $expandNextHash = false; - - private array $displayOptions = [ - 'fileLinkFormat' => null, - ]; - - private bool $handlesHrefGracefully; - - public function __construct($output = null, ?string $charset = null, int $flags = 0) - { - parent::__construct($output, $charset, $flags); - - if ('\\' === \DIRECTORY_SEPARATOR && !$this->isWindowsTrueColor()) { - // Use only the base 16 xterm colors when using ANSICON or standard Windows 10 CLI - $this->setStyles([ - 'default' => '31', - 'num' => '1;34', - 'const' => '1;31', - 'str' => '1;32', - 'note' => '34', - 'ref' => '1;30', - 'meta' => '35', - 'key' => '32', - 'index' => '34', - ]); - } - - $this->displayOptions['fileLinkFormat'] = \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: 'file://%f#L%l'; - } - - /** - * Enables/disables colored output. - * - * @return void - */ - public function setColors(bool $colors) - { - $this->colors = $colors; - } - - /** - * Sets the maximum number of characters per line for dumped strings. - * - * @return void - */ - public function setMaxStringWidth(int $maxStringWidth) - { - $this->maxStringWidth = $maxStringWidth; - } - - /** - * Configures styles. - * - * @param array $styles A map of style names to style definitions - * - * @return void - */ - public function setStyles(array $styles) - { - $this->styles = $styles + $this->styles; - } - - /** - * Configures display options. - * - * @param array $displayOptions A map of display options to customize the behavior - * - * @return void - */ - public function setDisplayOptions(array $displayOptions) - { - $this->displayOptions = $displayOptions + $this->displayOptions; - } - - /** - * @return void - */ - public function dumpScalar(Cursor $cursor, string $type, string|int|float|bool|null $value) - { - $this->dumpKey($cursor); - $this->collapseNextHash = $this->expandNextHash = false; - - $style = 'const'; - $attr = $cursor->attr; - - switch ($type) { - case 'default': - $style = 'default'; - break; - - case 'label': - $this->styles += ['label' => $this->styles['default']]; - $style = 'label'; - break; - - case 'integer': - $style = 'num'; - - if (isset($this->styles['integer'])) { - $style = 'integer'; - } - - break; - - case 'double': - $style = 'num'; - - if (isset($this->styles['float'])) { - $style = 'float'; - } - - $value = match (true) { - \INF === $value => 'INF', - -\INF === $value => '-INF', - is_nan($value) => 'NAN', - default => !str_contains($value = (string) $value, $this->decimalPoint) ? $value .= $this->decimalPoint.'0' : $value, - }; - break; - - case 'NULL': - $value = 'null'; - break; - - case 'boolean': - $value = $value ? 'true' : 'false'; - break; - - default: - $attr += ['value' => $this->utf8Encode($value)]; - $value = $this->utf8Encode($type); - break; - } - - $this->line .= $this->style($style, $value, $attr); - - $this->endValue($cursor); - } - - /** - * @return void - */ - public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut) - { - $this->dumpKey($cursor); - $this->collapseNextHash = $this->expandNextHash = false; - $attr = $cursor->attr; - - if ($bin) { - $str = $this->utf8Encode($str); - } - if ('' === $str) { - $this->line .= '""'; - if ($cut) { - $this->line .= '…'.$cut; - } - $this->endValue($cursor); - } else { - $attr += [ - 'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0, - 'binary' => $bin, - ]; - $str = $bin && str_contains($str, "\0") ? [$str] : explode("\n", $str); - if (isset($str[1]) && !isset($str[2]) && !isset($str[1][0])) { - unset($str[1]); - $str[0] .= "\n"; - } - $m = \count($str) - 1; - $i = $lineCut = 0; - - if (self::DUMP_STRING_LENGTH & $this->flags) { - $this->line .= '('.$attr['length'].') '; - } - if ($bin) { - $this->line .= 'b'; - } - - if ($m) { - $this->line .= '"""'; - $this->dumpLine($cursor->depth); - } else { - $this->line .= '"'; - } - - foreach ($str as $str) { - if ($i < $m) { - $str .= "\n"; - } - if (0 < $this->maxStringWidth && $this->maxStringWidth < $len = mb_strlen($str, 'UTF-8')) { - $str = mb_substr($str, 0, $this->maxStringWidth, 'UTF-8'); - $lineCut = $len - $this->maxStringWidth; - } - if ($m && 0 < $cursor->depth) { - $this->line .= $this->indentPad; - } - if ('' !== $str) { - $this->line .= $this->style('str', $str, $attr); - } - if ($i++ == $m) { - if ($m) { - if ('' !== $str) { - $this->dumpLine($cursor->depth); - if (0 < $cursor->depth) { - $this->line .= $this->indentPad; - } - } - $this->line .= '"""'; - } else { - $this->line .= '"'; - } - if ($cut < 0) { - $this->line .= '…'; - $lineCut = 0; - } elseif ($cut) { - $lineCut += $cut; - } - } - if ($lineCut) { - $this->line .= '…'.$lineCut; - $lineCut = 0; - } - - if ($i > $m) { - $this->endValue($cursor); - } else { - $this->dumpLine($cursor->depth); - } - } - } - } - - /** - * @return void - */ - public function enterHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild) - { - $this->colors ??= $this->supportsColors(); - - $this->dumpKey($cursor); - $this->expandNextHash = false; - $attr = $cursor->attr; - - if ($this->collapseNextHash) { - $cursor->skipChildren = true; - $this->collapseNextHash = $hasChild = false; - } - - $class = $this->utf8Encode($class); - if (Cursor::HASH_OBJECT === $type) { - $prefix = $class && 'stdClass' !== $class ? $this->style('note', $class, $attr).(empty($attr['cut_hash']) ? ' {' : '') : '{'; - } elseif (Cursor::HASH_RESOURCE === $type) { - $prefix = $this->style('note', $class.' resource', $attr).($hasChild ? ' {' : ' '); - } else { - $prefix = $class && !(self::DUMP_LIGHT_ARRAY & $this->flags) ? $this->style('note', 'array:'.$class).' [' : '['; - } - - if (($cursor->softRefCount || 0 < $cursor->softRefHandle) && empty($attr['cut_hash'])) { - $prefix .= $this->style('ref', (Cursor::HASH_RESOURCE === $type ? '@' : '#').(0 < $cursor->softRefHandle ? $cursor->softRefHandle : $cursor->softRefTo), ['count' => $cursor->softRefCount]); - } elseif ($cursor->hardRefTo && !$cursor->refIndex && $class) { - $prefix .= $this->style('ref', '&'.$cursor->hardRefTo, ['count' => $cursor->hardRefCount]); - } elseif (!$hasChild && Cursor::HASH_RESOURCE === $type) { - $prefix = substr($prefix, 0, -1); - } - - $this->line .= $prefix; - - if ($hasChild) { - $this->dumpLine($cursor->depth); - } - } - - /** - * @return void - */ - public function leaveHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild, int $cut) - { - if (empty($cursor->attr['cut_hash'])) { - $this->dumpEllipsis($cursor, $hasChild, $cut); - $this->line .= Cursor::HASH_OBJECT === $type ? '}' : (Cursor::HASH_RESOURCE !== $type ? ']' : ($hasChild ? '}' : '')); - } - - $this->endValue($cursor); - } - - /** - * Dumps an ellipsis for cut children. - * - * @param bool $hasChild When the dump of the hash has child item - * @param int $cut The number of items the hash has been cut by - * - * @return void - */ - protected function dumpEllipsis(Cursor $cursor, bool $hasChild, int $cut) - { - if ($cut) { - $this->line .= ' …'; - if (0 < $cut) { - $this->line .= $cut; - } - if ($hasChild) { - $this->dumpLine($cursor->depth + 1); - } - } - } - - /** - * Dumps a key in a hash structure. - * - * @return void - */ - protected function dumpKey(Cursor $cursor) - { - if (null !== $key = $cursor->hashKey) { - if ($cursor->hashKeyIsBinary) { - $key = $this->utf8Encode($key); - } - $attr = ['binary' => $cursor->hashKeyIsBinary]; - $bin = $cursor->hashKeyIsBinary ? 'b' : ''; - $style = 'key'; - switch ($cursor->hashType) { - default: - case Cursor::HASH_INDEXED: - if (self::DUMP_LIGHT_ARRAY & $this->flags) { - break; - } - $style = 'index'; - // no break - case Cursor::HASH_ASSOC: - if (\is_int($key)) { - $this->line .= $this->style($style, $key).' => '; - } else { - $this->line .= $bin.'"'.$this->style($style, $key).'" => '; - } - break; - - case Cursor::HASH_RESOURCE: - $key = "\0~\0".$key; - // no break - case Cursor::HASH_OBJECT: - if (!isset($key[0]) || "\0" !== $key[0]) { - $this->line .= '+'.$bin.$this->style('public', $key).': '; - } elseif (0 < strpos($key, "\0", 1)) { - $key = explode("\0", substr($key, 1), 2); - - switch ($key[0][0]) { - case '+': // User inserted keys - $attr['dynamic'] = true; - $this->line .= '+'.$bin.'"'.$this->style('public', $key[1], $attr).'": '; - break 2; - case '~': - $style = 'meta'; - if (isset($key[0][1])) { - parse_str(substr($key[0], 1), $attr); - $attr += ['binary' => $cursor->hashKeyIsBinary]; - } - break; - case '*': - $style = 'protected'; - $bin = '#'.$bin; - break; - default: - $attr['class'] = $key[0]; - $style = 'private'; - $bin = '-'.$bin; - break; - } - - if (isset($attr['collapse'])) { - if ($attr['collapse']) { - $this->collapseNextHash = true; - } else { - $this->expandNextHash = true; - } - } - - $this->line .= $bin.$this->style($style, $key[1], $attr).($attr['separator'] ?? ': '); - } else { - // This case should not happen - $this->line .= '-'.$bin.'"'.$this->style('private', $key, ['class' => '']).'": '; - } - break; - } - - if ($cursor->hardRefTo) { - $this->line .= $this->style('ref', '&'.($cursor->hardRefCount ? $cursor->hardRefTo : ''), ['count' => $cursor->hardRefCount]).' '; - } - } - } - - /** - * Decorates a value with some style. - * - * @param string $style The type of style being applied - * @param string $value The value being styled - * @param array $attr Optional context information - */ - protected function style(string $style, string $value, array $attr = []): string - { - $this->colors ??= $this->supportsColors(); - - $this->handlesHrefGracefully ??= 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') - && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100) - && !isset($_SERVER['IDEA_INITIAL_DIRECTORY']); - - if (isset($attr['ellipsis'], $attr['ellipsis-type'])) { - $prefix = substr($value, 0, -$attr['ellipsis']); - if ('cli' === \PHP_SAPI && 'path' === $attr['ellipsis-type'] && isset($_SERVER[$pwd = '\\' === \DIRECTORY_SEPARATOR ? 'CD' : 'PWD']) && str_starts_with($prefix, $_SERVER[$pwd])) { - $prefix = '.'.substr($prefix, \strlen($_SERVER[$pwd])); - } - if (!empty($attr['ellipsis-tail'])) { - $prefix .= substr($value, -$attr['ellipsis'], $attr['ellipsis-tail']); - $value = substr($value, -$attr['ellipsis'] + $attr['ellipsis-tail']); - } else { - $value = substr($value, -$attr['ellipsis']); - } - - $value = $this->style('default', $prefix).$this->style($style, $value); - - goto href; - } - - $map = static::$controlCharsMap; - $startCchr = $this->colors ? "\033[m\033[{$this->styles['default']}m" : ''; - $endCchr = $this->colors ? "\033[m\033[{$this->styles[$style]}m" : ''; - $value = preg_replace_callback(static::$controlCharsRx, function ($c) use ($map, $startCchr, $endCchr) { - $s = $startCchr; - $c = $c[$i = 0]; - do { - $s .= $map[$c[$i]] ?? sprintf('\x%02X', \ord($c[$i])); - } while (isset($c[++$i])); - - return $s.$endCchr; - }, $value, -1, $cchrCount); - - if (!($attr['binary'] ?? false)) { - $value = preg_replace_callback(static::$unicodeCharsRx, function ($c) use (&$cchrCount, $startCchr, $endCchr) { - ++$cchrCount; - - return $startCchr.'\u{'.strtoupper(dechex(mb_ord($c[0]))).'}'.$endCchr; - }, $value); - } - - if ($this->colors && '' !== $value) { - if ($cchrCount && "\033" === $value[0]) { - $value = substr($value, \strlen($startCchr)); - } else { - $value = "\033[{$this->styles[$style]}m".$value; - } - if ($cchrCount && str_ends_with($value, $endCchr)) { - $value = substr($value, 0, -\strlen($endCchr)); - } else { - $value .= "\033[{$this->styles['default']}m"; - } - } - - href: - if ($this->colors && $this->handlesHrefGracefully) { - if (isset($attr['file']) && $href = $this->getSourceLink($attr['file'], $attr['line'] ?? 0)) { - if ('note' === $style) { - $value .= "\033]8;;{$href}\033\\^\033]8;;\033\\"; - } else { - $attr['href'] = $href; - } - } - if (isset($attr['href'])) { - if ('label' === $style) { - $value .= '^'; - } - $value = "\033]8;;{$attr['href']}\033\\{$value}\033]8;;\033\\"; - } - } - - if ('label' === $style && '' !== $value) { - $value .= ' '; - } - - return $value; - } - - protected function supportsColors(): bool - { - if ($this->outputStream !== static::$defaultOutput) { - return $this->hasColorSupport($this->outputStream); - } - if (isset(static::$defaultColors)) { - return static::$defaultColors; - } - if (isset($_SERVER['argv'][1])) { - $colors = $_SERVER['argv']; - $i = \count($colors); - while (--$i > 0) { - if (isset($colors[$i][5])) { - switch ($colors[$i]) { - case '--ansi': - case '--color': - case '--color=yes': - case '--color=force': - case '--color=always': - case '--colors=always': - return static::$defaultColors = true; - - case '--no-ansi': - case '--color=no': - case '--color=none': - case '--color=never': - case '--colors=never': - return static::$defaultColors = false; - } - } - } - } - - $h = stream_get_meta_data($this->outputStream) + ['wrapper_type' => null]; - $h = 'Output' === $h['stream_type'] && 'PHP' === $h['wrapper_type'] ? fopen('php://stdout', 'w') : $this->outputStream; - - return static::$defaultColors = $this->hasColorSupport($h); - } - - /** - * @return void - */ - protected function dumpLine(int $depth, bool $endOfValue = false) - { - if (null === $this->colors) { - $this->colors = $this->supportsColors(); - } - - if ($this->colors) { - $this->line = sprintf("\033[%sm%s\033[m", $this->styles['default'], $this->line); - } - parent::dumpLine($depth); - } - - /** - * @return void - */ - protected function endValue(Cursor $cursor) - { - if (-1 === $cursor->hashType) { - return; - } - - if (Stub::ARRAY_INDEXED === $cursor->hashType || Stub::ARRAY_ASSOC === $cursor->hashType) { - if (self::DUMP_TRAILING_COMMA & $this->flags && 0 < $cursor->depth) { - $this->line .= ','; - } elseif (self::DUMP_COMMA_SEPARATOR & $this->flags && 1 < $cursor->hashLength - $cursor->hashIndex) { - $this->line .= ','; - } - } - - $this->dumpLine($cursor->depth, true); - } - - /** - * Returns true if the stream supports colorization. - * - * Reference: Composer\XdebugHandler\Process::supportsColor - * https://github.com/composer/xdebug-handler - */ - private function hasColorSupport(mixed $stream): bool - { - if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) { - return false; - } - - // Follow https://no-color.org/ - if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { - return false; - } - - if ('Hyper' === getenv('TERM_PROGRAM')) { - return true; - } - - if (\DIRECTORY_SEPARATOR === '\\') { - return (\function_exists('sapi_windows_vt100_support') - && @sapi_windows_vt100_support($stream)) - || false !== getenv('ANSICON') - || 'ON' === getenv('ConEmuANSI') - || 'xterm' === getenv('TERM'); - } - - return stream_isatty($stream); - } - - /** - * Returns true if the Windows terminal supports true color. - * - * Note that this does not check an output stream, but relies on environment - * variables from known implementations, or a PHP and Windows version that - * supports true color. - */ - private function isWindowsTrueColor(): bool - { - $result = 183 <= getenv('ANSICON_VER') - || 'ON' === getenv('ConEmuANSI') - || 'xterm' === getenv('TERM') - || 'Hyper' === getenv('TERM_PROGRAM'); - - if (!$result) { - $version = sprintf( - '%s.%s.%s', - PHP_WINDOWS_VERSION_MAJOR, - PHP_WINDOWS_VERSION_MINOR, - PHP_WINDOWS_VERSION_BUILD - ); - $result = $version >= '10.0.15063'; - } - - return $result; - } - - private function getSourceLink(string $file, int $line): string|false - { - if ($fmt = $this->displayOptions['fileLinkFormat']) { - return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : ($fmt->format($file, $line) ?: 'file://'.$file.'#L'.$line); - } - - return false; - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/CliContextProvider.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/CliContextProvider.php deleted file mode 100644 index 38f878971..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/CliContextProvider.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper\ContextProvider; - -/** - * Tries to provide context on CLI. - * - * @author Maxime Steinhausser - */ -final class CliContextProvider implements ContextProviderInterface -{ - public function getContext(): ?array - { - if ('cli' !== \PHP_SAPI) { - return null; - } - - return [ - 'command_line' => $commandLine = implode(' ', $_SERVER['argv'] ?? []), - 'identifier' => hash('crc32b', $commandLine.$_SERVER['REQUEST_TIME_FLOAT']), - ]; - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/ContextProviderInterface.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/ContextProviderInterface.php deleted file mode 100644 index 532aa0f96..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/ContextProviderInterface.php +++ /dev/null @@ -1,22 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper\ContextProvider; - -/** - * Interface to provide contextual data about dump data clones sent to a server. - * - * @author Maxime Steinhausser - */ -interface ContextProviderInterface -{ - public function getContext(): ?array; -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/RequestContextProvider.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/RequestContextProvider.php deleted file mode 100644 index 69dff067b..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/RequestContextProvider.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper\ContextProvider; - -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\VarDumper\Caster\ReflectionCaster; -use Symfony\Component\VarDumper\Cloner\VarCloner; - -/** - * Tries to provide context from a request. - * - * @author Maxime Steinhausser - */ -final class RequestContextProvider implements ContextProviderInterface -{ - private RequestStack $requestStack; - private VarCloner $cloner; - - public function __construct(RequestStack $requestStack) - { - $this->requestStack = $requestStack; - $this->cloner = new VarCloner(); - $this->cloner->setMaxItems(0); - $this->cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); - } - - public function getContext(): ?array - { - if (null === $request = $this->requestStack->getCurrentRequest()) { - return null; - } - - $controller = $request->attributes->get('_controller'); - - return [ - 'uri' => $request->getUri(), - 'method' => $request->getMethod(), - 'controller' => $controller ? $this->cloner->cloneVar($controller) : $controller, - 'identifier' => spl_object_hash($request), - ]; - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php deleted file mode 100644 index cadddfac4..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextProvider/SourceContextProvider.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper\ContextProvider; - -use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter as LegacyFileLinkFormatter; -use Symfony\Component\VarDumper\Cloner\VarCloner; -use Symfony\Component\VarDumper\Dumper\HtmlDumper; -use Symfony\Component\VarDumper\VarDumper; -use Twig\Template; - -/** - * Tries to provide context from sources (class name, file, line, code excerpt, ...). - * - * @author Nicolas Grekas - * @author Maxime Steinhausser - */ -final class SourceContextProvider implements ContextProviderInterface -{ - private int $limit; - private ?string $charset; - private ?string $projectDir; - private FileLinkFormatter|LegacyFileLinkFormatter|null $fileLinkFormatter; - - public function __construct(?string $charset = null, ?string $projectDir = null, FileLinkFormatter|LegacyFileLinkFormatter|null $fileLinkFormatter = null, int $limit = 9) - { - $this->charset = $charset; - $this->projectDir = $projectDir; - $this->fileLinkFormatter = $fileLinkFormatter; - $this->limit = $limit; - } - - public function getContext(): ?array - { - $trace = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, $this->limit); - - $file = $trace[1]['file']; - $line = $trace[1]['line']; - $name = '-' === $file || 'Standard input code' === $file ? 'Standard input code' : false; - $fileExcerpt = false; - - for ($i = 2; $i < $this->limit; ++$i) { - if (isset($trace[$i]['class'], $trace[$i]['function']) - && 'dump' === $trace[$i]['function'] - && VarDumper::class === $trace[$i]['class'] - ) { - $file = $trace[$i]['file'] ?? $file; - $line = $trace[$i]['line'] ?? $line; - - while (++$i < $this->limit) { - if (isset($trace[$i]['function'], $trace[$i]['file']) && empty($trace[$i]['class']) && !str_starts_with($trace[$i]['function'], 'call_user_func')) { - $file = $trace[$i]['file']; - $line = $trace[$i]['line']; - - break; - } elseif (isset($trace[$i]['object']) && $trace[$i]['object'] instanceof Template) { - $template = $trace[$i]['object']; - $name = $template->getTemplateName(); - $src = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getCode() : (method_exists($template, 'getSource') ? $template->getSource() : false); - $info = $template->getDebugInfo(); - if (isset($info[$trace[$i - 1]['line']])) { - $line = $info[$trace[$i - 1]['line']]; - $file = method_exists($template, 'getSourceContext') ? $template->getSourceContext()->getPath() : null; - - if ($src) { - $src = explode("\n", $src); - $fileExcerpt = []; - - for ($i = max($line - 3, 1), $max = min($line + 3, \count($src)); $i <= $max; ++$i) { - $fileExcerpt[] = ''.$this->htmlEncode($src[$i - 1]).''; - } - - $fileExcerpt = '
      '.implode("\n", $fileExcerpt).'
    '; - } - } - break; - } - } - break; - } - } - - if (false === $name) { - $name = str_replace('\\', '/', $file); - $name = substr($name, strrpos($name, '/') + 1); - } - - $context = ['name' => $name, 'file' => $file, 'line' => $line]; - $context['file_excerpt'] = $fileExcerpt; - - if (null !== $this->projectDir) { - $context['project_dir'] = $this->projectDir; - if (str_starts_with($file, $this->projectDir)) { - $context['file_relative'] = ltrim(substr($file, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); - } - } - - if ($this->fileLinkFormatter && $fileLink = $this->fileLinkFormatter->format($context['file'], $context['line'])) { - $context['file_link'] = $fileLink; - } - - return $context; - } - - private function htmlEncode(string $s): string - { - $html = ''; - - $dumper = new HtmlDumper(function ($line) use (&$html) { $html .= $line; }, $this->charset); - $dumper->setDumpHeader(''); - $dumper->setDumpBoundaries('', ''); - - $cloner = new VarCloner(); - $dumper->dump($cloner->cloneVar($s)); - - return substr(strip_tags($html), 1, -1); - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextualizedDumper.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextualizedDumper.php deleted file mode 100644 index 84cfb4259..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ContextualizedDumper.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper; - -use Symfony\Component\VarDumper\Cloner\Data; -use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface; - -/** - * @author Kévin Thérage - */ -class ContextualizedDumper implements DataDumperInterface -{ - private DataDumperInterface $wrappedDumper; - private array $contextProviders; - - /** - * @param ContextProviderInterface[] $contextProviders - */ - public function __construct(DataDumperInterface $wrappedDumper, array $contextProviders) - { - $this->wrappedDumper = $wrappedDumper; - $this->contextProviders = $contextProviders; - } - - /** - * @return string|null - */ - public function dump(Data $data) - { - $context = $data->getContext(); - foreach ($this->contextProviders as $contextProvider) { - $context[$contextProvider::class] = $contextProvider->getContext(); - } - - return $this->wrappedDumper->dump($data->withContext($context)); - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/DataDumperInterface.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/DataDumperInterface.php deleted file mode 100644 index df05b6af5..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/DataDumperInterface.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper; - -use Symfony\Component\VarDumper\Cloner\Data; - -/** - * DataDumperInterface for dumping Data objects. - * - * @author Nicolas Grekas - */ -interface DataDumperInterface -{ - /** - * @return string|null - */ - public function dump(Data $data); -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/HtmlDumper.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/HtmlDumper.php deleted file mode 100644 index ea09e6819..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/HtmlDumper.php +++ /dev/null @@ -1,998 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper; - -use Symfony\Component\VarDumper\Cloner\Cursor; -use Symfony\Component\VarDumper\Cloner\Data; - -/** - * HtmlDumper dumps variables as HTML. - * - * @author Nicolas Grekas - */ -class HtmlDumper extends CliDumper -{ - /** @var callable|resource|string|null */ - public static $defaultOutput = 'php://output'; - - protected static $themes = [ - 'dark' => [ - 'default' => 'background-color:#18171B; color:#FF8400; line-height:1.2em; font:12px Menlo, Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: break-all', - 'num' => 'font-weight:bold; color:#1299DA', - 'const' => 'font-weight:bold', - 'str' => 'font-weight:bold; color:#56DB3A', - 'note' => 'color:#1299DA', - 'ref' => 'color:#A0A0A0', - 'public' => 'color:#FFFFFF', - 'protected' => 'color:#FFFFFF', - 'private' => 'color:#FFFFFF', - 'meta' => 'color:#B729D9', - 'key' => 'color:#56DB3A', - 'index' => 'color:#1299DA', - 'ellipsis' => 'color:#FF8400', - 'ns' => 'user-select:none;', - ], - 'light' => [ - 'default' => 'background:none; color:#CC7832; line-height:1.2em; font:12px Menlo, Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:99999; word-break: break-all', - 'num' => 'font-weight:bold; color:#1299DA', - 'const' => 'font-weight:bold', - 'str' => 'font-weight:bold; color:#629755;', - 'note' => 'color:#6897BB', - 'ref' => 'color:#6E6E6E', - 'public' => 'color:#262626', - 'protected' => 'color:#262626', - 'private' => 'color:#262626', - 'meta' => 'color:#B729D9', - 'key' => 'color:#789339', - 'index' => 'color:#1299DA', - 'ellipsis' => 'color:#CC7832', - 'ns' => 'user-select:none;', - ], - ]; - - protected $dumpHeader; - protected $dumpPrefix = '
    ';
    -    protected $dumpSuffix = '
    '; - protected $dumpId = 'sf-dump'; - protected $colors = true; - protected $headerIsDumped = false; - protected $lastDepth = -1; - protected $styles; - - private array $displayOptions = [ - 'maxDepth' => 1, - 'maxStringLength' => 160, - 'fileLinkFormat' => null, - ]; - private array $extraDisplayOptions = []; - - public function __construct($output = null, ?string $charset = null, int $flags = 0) - { - AbstractDumper::__construct($output, $charset, $flags); - $this->dumpId = 'sf-dump-'.mt_rand(); - $this->displayOptions['fileLinkFormat'] = \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); - $this->styles = static::$themes['dark'] ?? self::$themes['dark']; - } - - /** - * @return void - */ - public function setStyles(array $styles) - { - $this->headerIsDumped = false; - $this->styles = $styles + $this->styles; - } - - /** - * @return void - */ - public function setTheme(string $themeName) - { - if (!isset(static::$themes[$themeName])) { - throw new \InvalidArgumentException(sprintf('Theme "%s" does not exist in class "%s".', $themeName, static::class)); - } - - $this->setStyles(static::$themes[$themeName]); - } - - /** - * Configures display options. - * - * @param array $displayOptions A map of display options to customize the behavior - * - * @return void - */ - public function setDisplayOptions(array $displayOptions) - { - $this->headerIsDumped = false; - $this->displayOptions = $displayOptions + $this->displayOptions; - } - - /** - * Sets an HTML header that will be dumped once in the output stream. - * - * @return void - */ - public function setDumpHeader(?string $header) - { - $this->dumpHeader = $header; - } - - /** - * Sets an HTML prefix and suffix that will encapse every single dump. - * - * @return void - */ - public function setDumpBoundaries(string $prefix, string $suffix) - { - $this->dumpPrefix = $prefix; - $this->dumpSuffix = $suffix; - } - - public function dump(Data $data, $output = null, array $extraDisplayOptions = []): ?string - { - $this->extraDisplayOptions = $extraDisplayOptions; - $result = parent::dump($data, $output); - $this->dumpId = 'sf-dump-'.mt_rand(); - - return $result; - } - - /** - * Dumps the HTML header. - * - * @return string - */ - protected function getDumpHeader() - { - $this->headerIsDumped = $this->outputStream ?? $this->lineDumper; - - if (null !== $this->dumpHeader) { - return $this->dumpHeader; - } - - $line = str_replace('{$options}', json_encode($this->displayOptions, \JSON_FORCE_OBJECT), <<<'EOHTML' -'.$this->dumpHeader; - } - - /** - * @return void - */ - public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut) - { - if ('' === $str && isset($cursor->attr['img-data'], $cursor->attr['content-type'])) { - $this->dumpKey($cursor); - $this->line .= $this->style('default', $cursor->attr['img-size'] ?? '', []); - $this->line .= $cursor->depth >= $this->displayOptions['maxDepth'] ? ' ' : ' '; - $this->endValue($cursor); - $this->line .= $this->indentPad; - $this->line .= sprintf('', $cursor->attr['content-type'], base64_encode($cursor->attr['img-data'])); - $this->endValue($cursor); - } else { - parent::dumpString($cursor, $str, $bin, $cut); - } - } - - /** - * @return void - */ - public function enterHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild) - { - if (Cursor::HASH_OBJECT === $type) { - $cursor->attr['depth'] = $cursor->depth; - } - parent::enterHash($cursor, $type, $class, false); - - if ($cursor->skipChildren || $cursor->depth >= $this->displayOptions['maxDepth']) { - $cursor->skipChildren = false; - $eol = ' class=sf-dump-compact>'; - } else { - $this->expandNextHash = false; - $eol = ' class=sf-dump-expanded>'; - } - - if ($hasChild) { - $this->line .= 'dumpId, $r); - } - $this->line .= $eol; - $this->dumpLine($cursor->depth); - } - } - - /** - * @return void - */ - public function leaveHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild, int $cut) - { - $this->dumpEllipsis($cursor, $hasChild, $cut); - if ($hasChild) { - $this->line .= ''; - } - parent::leaveHash($cursor, $type, $class, $hasChild, 0); - } - - protected function style(string $style, string $value, array $attr = []): string - { - if ('' === $value && ('label' !== $style || !isset($attr['file']) && !isset($attr['href']))) { - return ''; - } - - $v = esc($value); - - if ('ref' === $style) { - if (empty($attr['count'])) { - return sprintf('%s', $v); - } - $r = ('#' !== $v[0] ? 1 - ('@' !== $v[0]) : 2).substr($value, 1); - - return sprintf('%s', $this->dumpId, $r, 1 + $attr['count'], $v); - } - - if ('const' === $style && isset($attr['value'])) { - $style .= sprintf(' title="%s"', esc(\is_scalar($attr['value']) ? $attr['value'] : json_encode($attr['value']))); - } elseif ('public' === $style) { - $style .= sprintf(' title="%s"', empty($attr['dynamic']) ? 'Public property' : 'Runtime added dynamic property'); - } elseif ('str' === $style && 1 < $attr['length']) { - $style .= sprintf(' title="%d%s characters"', $attr['length'], $attr['binary'] ? ' binary or non-UTF-8' : ''); - } elseif ('note' === $style && 0 < ($attr['depth'] ?? 0) && false !== $c = strrpos($value, '\\')) { - $style .= ' title=""'; - $attr += [ - 'ellipsis' => \strlen($value) - $c, - 'ellipsis-type' => 'note', - 'ellipsis-tail' => 1, - ]; - } elseif ('protected' === $style) { - $style .= ' title="Protected property"'; - } elseif ('meta' === $style && isset($attr['title'])) { - $style .= sprintf(' title="%s"', esc($this->utf8Encode($attr['title']))); - } elseif ('private' === $style) { - $style .= sprintf(' title="Private property defined in class: `%s`"', esc($this->utf8Encode($attr['class']))); - } - - if (isset($attr['ellipsis'])) { - $class = 'sf-dump-ellipsis'; - if (isset($attr['ellipsis-type'])) { - $class = sprintf('"%s sf-dump-ellipsis-%s"', $class, $attr['ellipsis-type']); - } - $label = esc(substr($value, -$attr['ellipsis'])); - $style = str_replace(' title="', " title=\"$v\n", $style); - $v = sprintf('%s', $class, substr($v, 0, -\strlen($label))); - - if (!empty($attr['ellipsis-tail'])) { - $tail = \strlen(esc(substr($value, -$attr['ellipsis'], $attr['ellipsis-tail']))); - $v .= sprintf('%s%s', $class, substr($label, 0, $tail), substr($label, $tail)); - } else { - $v .= $label; - } - } - - $map = static::$controlCharsMap; - $v = "".preg_replace_callback(static::$controlCharsRx, function ($c) use ($map) { - $s = $b = ''; - }, $v).''; - - if (!($attr['binary'] ?? false)) { - $v = preg_replace_callback(static::$unicodeCharsRx, function ($c) { - return '\u{'.strtoupper(dechex(mb_ord($c[0]))).'}'; - }, $v); - } - - if (isset($attr['file']) && $href = $this->getSourceLink($attr['file'], $attr['line'] ?? 0)) { - $attr['href'] = $href; - } - if (isset($attr['href'])) { - if ('label' === $style) { - $v .= '^'; - } - $target = isset($attr['file']) ? '' : ' target="_blank"'; - $v = sprintf('%s', esc($this->utf8Encode($attr['href'])), $target, $v); - } - if (isset($attr['lang'])) { - $v = sprintf('%s', esc($attr['lang']), $v); - } - if ('label' === $style) { - $v .= ' '; - } - - return $v; - } - - /** - * @return void - */ - protected function dumpLine(int $depth, bool $endOfValue = false) - { - if (-1 === $this->lastDepth) { - $this->line = sprintf($this->dumpPrefix, $this->dumpId, $this->indentPad).$this->line; - } - if ($this->headerIsDumped !== ($this->outputStream ?? $this->lineDumper)) { - $this->line = $this->getDumpHeader().$this->line; - } - - if (-1 === $depth) { - $args = ['"'.$this->dumpId.'"']; - if ($this->extraDisplayOptions) { - $args[] = json_encode($this->extraDisplayOptions, \JSON_FORCE_OBJECT); - } - // Replace is for BC - $this->line .= sprintf(str_replace('"%s"', '%s', $this->dumpSuffix), implode(', ', $args)); - } - $this->lastDepth = $depth; - - $this->line = mb_encode_numericentity($this->line, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8'); - - if (-1 === $depth) { - AbstractDumper::dumpLine(0); - } - AbstractDumper::dumpLine($depth); - } - - private function getSourceLink(string $file, int $line): string|false - { - $options = $this->extraDisplayOptions + $this->displayOptions; - - if ($fmt = $options['fileLinkFormat']) { - return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : $fmt->format($file, $line); - } - - return false; - } -} - -function esc(string $str): string -{ - return htmlspecialchars($str, \ENT_QUOTES, 'UTF-8'); -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ServerDumper.php b/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ServerDumper.php deleted file mode 100644 index 60fdd7ac3..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Dumper/ServerDumper.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Dumper; - -use Symfony\Component\VarDumper\Cloner\Data; -use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface; -use Symfony\Component\VarDumper\Server\Connection; - -/** - * ServerDumper forwards serialized Data clones to a server. - * - * @author Maxime Steinhausser - */ -class ServerDumper implements DataDumperInterface -{ - private Connection $connection; - private ?DataDumperInterface $wrappedDumper; - - /** - * @param string $host The server host - * @param DataDumperInterface|null $wrappedDumper A wrapped instance used whenever we failed contacting the server - * @param ContextProviderInterface[] $contextProviders Context providers indexed by context name - */ - public function __construct(string $host, ?DataDumperInterface $wrappedDumper = null, array $contextProviders = []) - { - $this->connection = new Connection($host, $contextProviders); - $this->wrappedDumper = $wrappedDumper; - } - - public function getContextProviders(): array - { - return $this->connection->getContextProviders(); - } - - /** - * @return string|null - */ - public function dump(Data $data) - { - if (!$this->connection->write($data) && $this->wrappedDumper) { - return $this->wrappedDumper->dump($data); - } - - return null; - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Exception/ThrowingCasterException.php b/data/web/inc/lib/vendor/symfony/var-dumper/Exception/ThrowingCasterException.php deleted file mode 100644 index fd8eca9f1..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Exception/ThrowingCasterException.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Exception; - -/** - * @author Nicolas Grekas - */ -class ThrowingCasterException extends \Exception -{ - /** - * @param \Throwable $prev The exception thrown from the caster - */ - public function __construct(\Throwable $prev) - { - parent::__construct('Unexpected '.$prev::class.' thrown from a caster: '.$prev->getMessage(), 0, $prev); - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/README.md b/data/web/inc/lib/vendor/symfony/var-dumper/README.md deleted file mode 100644 index a0da8c9ab..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/README.md +++ /dev/null @@ -1,15 +0,0 @@ -VarDumper Component -=================== - -The VarDumper component provides mechanisms for walking through any arbitrary -PHP variable. It provides a better `dump()` function that you can use instead -of `var_dump()`. - -Resources ---------- - - * [Documentation](https://symfony.com/doc/current/components/var_dumper/introduction.html) - * [Contributing](https://symfony.com/doc/current/contributing/index.html) - * [Report issues](https://github.com/symfony/symfony/issues) and - [send Pull Requests](https://github.com/symfony/symfony/pulls) - in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Resources/bin/var-dump-server b/data/web/inc/lib/vendor/symfony/var-dumper/Resources/bin/var-dump-server deleted file mode 100755 index f398fcef7..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Resources/bin/var-dump-server +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env php - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -if ('cli' !== PHP_SAPI) { - throw new Exception('This script must be run from the command line.'); -} - -/** - * Starts a dump server to collect and output dumps on a single place with multiple formats support. - * - * @author Maxime Steinhausser - */ - -use Psr\Log\LoggerInterface; -use Symfony\Component\Console\Application; -use Symfony\Component\Console\Input\ArgvInput; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Logger\ConsoleLogger; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\VarDumper\Command\ServerDumpCommand; -use Symfony\Component\VarDumper\Server\DumpServer; - -function includeIfExists(string $file): bool -{ - return file_exists($file) && include $file; -} - -if ( - !includeIfExists(__DIR__ . '/../../../../autoload.php') && - !includeIfExists(__DIR__ . '/../../vendor/autoload.php') && - !includeIfExists(__DIR__ . '/../../../../../../vendor/autoload.php') -) { - fwrite(STDERR, 'Install dependencies using Composer.'.PHP_EOL); - exit(1); -} - -if (!class_exists(Application::class)) { - fwrite(STDERR, 'You need the "symfony/console" component in order to run the VarDumper server.'.PHP_EOL); - exit(1); -} - -$input = new ArgvInput(); -$output = new ConsoleOutput(); -$defaultHost = '127.0.0.1:9912'; -$host = $input->getParameterOption(['--host'], $_SERVER['VAR_DUMPER_SERVER'] ?? $defaultHost, true); -$logger = interface_exists(LoggerInterface::class) ? new ConsoleLogger($output->getErrorOutput()) : null; - -$app = new Application(); - -$app->getDefinition()->addOption( - new InputOption('--host', null, InputOption::VALUE_REQUIRED, 'The address the server should listen to', $defaultHost) -); - -$app->add($command = new ServerDumpCommand(new DumpServer($host, $logger))) - ->getApplication() - ->setDefaultCommand($command->getName(), true) - ->run($input, $output) -; diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Resources/css/htmlDescriptor.css b/data/web/inc/lib/vendor/symfony/var-dumper/Resources/css/htmlDescriptor.css deleted file mode 100644 index 8f706d640..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Resources/css/htmlDescriptor.css +++ /dev/null @@ -1,130 +0,0 @@ -body { - display: flex; - flex-direction: column-reverse; - justify-content: flex-end; - max-width: 1140px; - margin: auto; - padding: 15px; - word-wrap: break-word; - background-color: #F9F9F9; - color: #222; - font-family: Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 1.4; -} -p { - margin: 0; -} -a { - color: #218BC3; - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -.text-small { - font-size: 12px !important; -} -article { - margin: 5px; - margin-bottom: 10px; -} -article > header > .row { - display: flex; - flex-direction: row; - align-items: baseline; - margin-bottom: 10px; -} -article > header > .row > .col { - flex: 1; - display: flex; - align-items: baseline; -} -article > header > .row > h2 { - font-size: 14px; - color: #222; - font-weight: normal; - font-family: "Lucida Console", monospace, sans-serif; - word-break: break-all; - margin: 20px 5px 0 0; - user-select: all; -} -article > header > .row > h2 > code { - white-space: nowrap; - user-select: none; - color: #cc2255; - background-color: #f7f7f9; - border: 1px solid #e1e1e8; - border-radius: 3px; - margin-right: 5px; - padding: 0 3px; -} -article > header > .row > time.col { - flex: 0; - text-align: right; - white-space: nowrap; - color: #999; - font-style: italic; -} -article > header ul.tags { - list-style: none; - padding: 0; - margin: 0; - font-size: 12px; -} -article > header ul.tags > li { - user-select: all; - margin-bottom: 2px; -} -article > header ul.tags > li > span.badge { - display: inline-block; - padding: .25em .4em; - margin-right: 5px; - border-radius: 4px; - background-color: #6c757d3b; - color: #524d4d; - font-size: 12px; - text-align: center; - font-weight: 700; - line-height: 1; - white-space: nowrap; - vertical-align: baseline; - user-select: none; -} -article > section.body { - border: 1px solid #d8d8d8; - background: #FFF; - padding: 10px; - border-radius: 3px; -} -pre.sf-dump { - border-radius: 3px; - margin-bottom: 0; -} -.hidden { - display: none !important; -} -.dumped-tag > .sf-dump { - display: inline-block; - margin: 0; - padding: 1px 5px; - line-height: 1.4; - vertical-align: top; - background-color: transparent; - user-select: auto; -} -.dumped-tag > pre.sf-dump, -.dumped-tag > .sf-dump-default { - color: #CC7832; - background: none; -} -.dumped-tag > .sf-dump .sf-dump-str { color: #629755; } -.dumped-tag > .sf-dump .sf-dump-private, -.dumped-tag > .sf-dump .sf-dump-protected, -.dumped-tag > .sf-dump .sf-dump-public { color: #262626; } -.dumped-tag > .sf-dump .sf-dump-note { color: #6897BB; } -.dumped-tag > .sf-dump .sf-dump-key { color: #789339; } -.dumped-tag > .sf-dump .sf-dump-ref { color: #6E6E6E; } -.dumped-tag > .sf-dump .sf-dump-ellipsis { color: #CC7832; max-width: 100em; } -.dumped-tag > .sf-dump .sf-dump-ellipsis-path { max-width: 5em; } -.dumped-tag > .sf-dump .sf-dump-ns { user-select: none; } diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Resources/functions/dump.php b/data/web/inc/lib/vendor/symfony/var-dumper/Resources/functions/dump.php deleted file mode 100644 index f2ff74c0c..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Resources/functions/dump.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -use Symfony\Component\VarDumper\Caster\ScalarStub; -use Symfony\Component\VarDumper\VarDumper; - -if (!function_exists('dump')) { - /** - * @author Nicolas Grekas - * @author Alexandre Daubois - */ - function dump(mixed ...$vars): mixed - { - if (!$vars) { - VarDumper::dump(new ScalarStub('🐛')); - - return null; - } - - if (array_key_exists(0, $vars) && 1 === count($vars)) { - VarDumper::dump($vars[0]); - $k = 0; - } else { - foreach ($vars as $k => $v) { - VarDumper::dump($v, is_int($k) ? 1 + $k : $k); - } - } - - if (1 < count($vars)) { - return $vars; - } - - return $vars[$k]; - } -} - -if (!function_exists('dd')) { - function dd(mixed ...$vars): never - { - if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) && !headers_sent()) { - header('HTTP/1.1 500 Internal Server Error'); - } - - if (array_key_exists(0, $vars) && 1 === count($vars)) { - VarDumper::dump($vars[0]); - } else { - foreach ($vars as $k => $v) { - VarDumper::dump($v, is_int($k) ? 1 + $k : $k); - } - } - - exit(1); - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Resources/js/htmlDescriptor.js b/data/web/inc/lib/vendor/symfony/var-dumper/Resources/js/htmlDescriptor.js deleted file mode 100644 index 63101e57c..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Resources/js/htmlDescriptor.js +++ /dev/null @@ -1,10 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - let prev = null; - Array.from(document.getElementsByTagName('article')).reverse().forEach(function (article) { - const dedupId = article.dataset.dedupId; - if (dedupId === prev) { - article.getElementsByTagName('header')[0].classList.add('hidden'); - } - prev = dedupId; - }); -}); diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Server/Connection.php b/data/web/inc/lib/vendor/symfony/var-dumper/Server/Connection.php deleted file mode 100644 index 4383278c9..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Server/Connection.php +++ /dev/null @@ -1,97 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Server; - -use Symfony\Component\VarDumper\Cloner\Data; -use Symfony\Component\VarDumper\Dumper\ContextProvider\ContextProviderInterface; - -/** - * Forwards serialized Data clones to a server. - * - * @author Maxime Steinhausser - */ -class Connection -{ - private string $host; - private array $contextProviders; - - /** - * @var resource|null - */ - private $socket; - - /** - * @param string $host The server host - * @param ContextProviderInterface[] $contextProviders Context providers indexed by context name - */ - public function __construct(string $host, array $contextProviders = []) - { - if (!str_contains($host, '://')) { - $host = 'tcp://'.$host; - } - - $this->host = $host; - $this->contextProviders = $contextProviders; - } - - public function getContextProviders(): array - { - return $this->contextProviders; - } - - public function write(Data $data): bool - { - $socketIsFresh = !$this->socket; - if (!$this->socket = $this->socket ?: $this->createSocket()) { - return false; - } - - $context = ['timestamp' => microtime(true)]; - foreach ($this->contextProviders as $name => $provider) { - $context[$name] = $provider->getContext(); - } - $context = array_filter($context); - $encodedPayload = base64_encode(serialize([$data, $context]))."\n"; - - set_error_handler(static fn () => null); - try { - if (-1 !== stream_socket_sendto($this->socket, $encodedPayload)) { - return true; - } - if (!$socketIsFresh) { - stream_socket_shutdown($this->socket, \STREAM_SHUT_RDWR); - fclose($this->socket); - $this->socket = $this->createSocket(); - } - if (-1 !== stream_socket_sendto($this->socket, $encodedPayload)) { - return true; - } - } finally { - restore_error_handler(); - } - - return false; - } - - /** - * @return resource|null - */ - private function createSocket() - { - set_error_handler(static fn () => null); - try { - return stream_socket_client($this->host, $errno, $errstr, 3) ?: null; - } finally { - restore_error_handler(); - } - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Server/DumpServer.php b/data/web/inc/lib/vendor/symfony/var-dumper/Server/DumpServer.php deleted file mode 100644 index a9228a2ef..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Server/DumpServer.php +++ /dev/null @@ -1,109 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Server; - -use Psr\Log\LoggerInterface; -use Symfony\Component\VarDumper\Cloner\Data; -use Symfony\Component\VarDumper\Cloner\Stub; - -/** - * A server collecting Data clones sent by a ServerDumper. - * - * @author Maxime Steinhausser - * - * @final - */ -class DumpServer -{ - private string $host; - private ?LoggerInterface $logger; - - /** - * @var resource|null - */ - private $socket; - - public function __construct(string $host, ?LoggerInterface $logger = null) - { - if (!str_contains($host, '://')) { - $host = 'tcp://'.$host; - } - - $this->host = $host; - $this->logger = $logger; - } - - public function start(): void - { - if (!$this->socket = stream_socket_server($this->host, $errno, $errstr)) { - throw new \RuntimeException(sprintf('Server start failed on "%s": ', $this->host).$errstr.' '.$errno); - } - } - - public function listen(callable $callback): void - { - if (null === $this->socket) { - $this->start(); - } - - foreach ($this->getMessages() as $clientId => $message) { - $this->logger?->info('Received a payload from client {clientId}', ['clientId' => $clientId]); - - $payload = @unserialize(base64_decode($message), ['allowed_classes' => [Data::class, Stub::class]]); - - // Impossible to decode the message, give up. - if (false === $payload) { - $this->logger?->warning('Unable to decode a message from {clientId} client.', ['clientId' => $clientId]); - - continue; - } - - if (!\is_array($payload) || \count($payload) < 2 || !$payload[0] instanceof Data || !\is_array($payload[1])) { - $this->logger?->warning('Invalid payload from {clientId} client. Expected an array of two elements (Data $data, array $context)', ['clientId' => $clientId]); - - continue; - } - - [$data, $context] = $payload; - - $callback($data, $context, $clientId); - } - } - - public function getHost(): string - { - return $this->host; - } - - private function getMessages(): iterable - { - $sockets = [(int) $this->socket => $this->socket]; - $write = []; - - while (true) { - $read = $sockets; - stream_select($read, $write, $write, null); - - foreach ($read as $stream) { - if ($this->socket === $stream) { - $stream = stream_socket_accept($this->socket); - $sockets[(int) $stream] = $stream; - } elseif (feof($stream)) { - unset($sockets[(int) $stream]); - fclose($stream); - } else { - yield (int) $stream => fgets($stream); - } - } - } - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/Test/VarDumperTestTrait.php b/data/web/inc/lib/vendor/symfony/var-dumper/Test/VarDumperTestTrait.php deleted file mode 100644 index 4475efd12..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/Test/VarDumperTestTrait.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper\Test; - -use Symfony\Component\VarDumper\Cloner\VarCloner; -use Symfony\Component\VarDumper\Dumper\CliDumper; - -/** - * @author Nicolas Grekas - */ -trait VarDumperTestTrait -{ - /** - * @internal - */ - private array $varDumperConfig = [ - 'casters' => [], - 'flags' => null, - ]; - - protected function setUpVarDumper(array $casters, ?int $flags = null): void - { - $this->varDumperConfig['casters'] = $casters; - $this->varDumperConfig['flags'] = $flags; - } - - /** - * @after - */ - protected function tearDownVarDumper(): void - { - $this->varDumperConfig['casters'] = []; - $this->varDumperConfig['flags'] = null; - } - - public function assertDumpEquals(mixed $expected, mixed $data, int $filter = 0, string $message = '') - { - $this->assertSame($this->prepareExpectation($expected, $filter), $this->getDump($data, null, $filter), $message); - } - - public function assertDumpMatchesFormat(mixed $expected, mixed $data, int $filter = 0, string $message = '') - { - $this->assertStringMatchesFormat($this->prepareExpectation($expected, $filter), $this->getDump($data, null, $filter), $message); - } - - protected function getDump(mixed $data, string|int|null $key = null, int $filter = 0): ?string - { - if (null === $flags = $this->varDumperConfig['flags']) { - $flags = getenv('DUMP_LIGHT_ARRAY') ? CliDumper::DUMP_LIGHT_ARRAY : 0; - $flags |= getenv('DUMP_STRING_LENGTH') ? CliDumper::DUMP_STRING_LENGTH : 0; - $flags |= getenv('DUMP_COMMA_SEPARATOR') ? CliDumper::DUMP_COMMA_SEPARATOR : 0; - } - - $cloner = new VarCloner(); - $cloner->addCasters($this->varDumperConfig['casters']); - $cloner->setMaxItems(-1); - $dumper = new CliDumper(null, null, $flags); - $dumper->setColors(false); - $data = $cloner->cloneVar($data, $filter)->withRefHandles(false); - if (null !== $key && null === $data = $data->seek($key)) { - return null; - } - - return rtrim($dumper->dump($data, true)); - } - - private function prepareExpectation(mixed $expected, int $filter): string - { - if (!\is_string($expected)) { - $expected = $this->getDump($expected, null, $filter); - } - - return rtrim($expected); - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/VarDumper.php b/data/web/inc/lib/vendor/symfony/var-dumper/VarDumper.php deleted file mode 100644 index e1400f150..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/VarDumper.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\VarDumper; - -use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\VarDumper\Caster\ReflectionCaster; -use Symfony\Component\VarDumper\Cloner\VarCloner; -use Symfony\Component\VarDumper\Dumper\CliDumper; -use Symfony\Component\VarDumper\Dumper\ContextProvider\CliContextProvider; -use Symfony\Component\VarDumper\Dumper\ContextProvider\RequestContextProvider; -use Symfony\Component\VarDumper\Dumper\ContextProvider\SourceContextProvider; -use Symfony\Component\VarDumper\Dumper\ContextualizedDumper; -use Symfony\Component\VarDumper\Dumper\HtmlDumper; -use Symfony\Component\VarDumper\Dumper\ServerDumper; - -// Load the global dump() function -require_once __DIR__.'/Resources/functions/dump.php'; - -/** - * @author Nicolas Grekas - */ -class VarDumper -{ - /** - * @var callable|null - */ - private static $handler; - - /** - * @param string|null $label - * - * @return mixed - */ - public static function dump(mixed $var/* , string $label = null */) - { - $label = 2 <= \func_num_args() ? func_get_arg(1) : null; - if (null === self::$handler) { - self::register(); - } - - return (self::$handler)($var, $label); - } - - public static function setHandler(?callable $callable = null): ?callable - { - if (1 > \func_num_args()) { - trigger_deprecation('symfony/var-dumper', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); - } - $prevHandler = self::$handler; - - // Prevent replacing the handler with expected format as soon as the env var was set: - if (isset($_SERVER['VAR_DUMPER_FORMAT'])) { - return $prevHandler; - } - - self::$handler = $callable; - - return $prevHandler; - } - - private static function register(): void - { - $cloner = new VarCloner(); - $cloner->addCasters(ReflectionCaster::UNSET_CLOSURE_FILE_INFO); - - $format = $_SERVER['VAR_DUMPER_FORMAT'] ?? null; - switch (true) { - case 'html' === $format: - $dumper = new HtmlDumper(); - break; - case 'cli' === $format: - $dumper = new CliDumper(); - break; - case 'server' === $format: - case $format && 'tcp' === parse_url($format, \PHP_URL_SCHEME): - $host = 'server' === $format ? $_SERVER['VAR_DUMPER_SERVER'] ?? '127.0.0.1:9912' : $format; - $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? new CliDumper() : new HtmlDumper(); - $dumper = new ServerDumper($host, $dumper, self::getDefaultContextProviders()); - break; - default: - $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? new CliDumper() : new HtmlDumper(); - } - - if (!$dumper instanceof ServerDumper) { - $dumper = new ContextualizedDumper($dumper, [new SourceContextProvider()]); - } - - self::$handler = function ($var, ?string $label = null) use ($cloner, $dumper) { - $var = $cloner->cloneVar($var); - - if (null !== $label) { - $var = $var->withContext(['label' => $label]); - } - - $dumper->dump($var); - }; - } - - private static function getDefaultContextProviders(): array - { - $contextProviders = []; - - if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) && class_exists(Request::class)) { - $requestStack = new RequestStack(); - $requestStack->push(Request::createFromGlobals()); - $contextProviders['request'] = new RequestContextProvider($requestStack); - } - - $fileLinkFormatter = class_exists(FileLinkFormatter::class) ? new FileLinkFormatter(null, $requestStack ?? null) : null; - - return $contextProviders + [ - 'cli' => new CliContextProvider(), - 'source' => new SourceContextProvider(null, null, $fileLinkFormatter), - ]; - } -} diff --git a/data/web/inc/lib/vendor/symfony/var-dumper/composer.json b/data/web/inc/lib/vendor/symfony/var-dumper/composer.json deleted file mode 100644 index e6166f86d..000000000 --- a/data/web/inc/lib/vendor/symfony/var-dumper/composer.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "symfony/var-dumper", - "type": "library", - "description": "Provides mechanisms for walking through any arbitrary PHP variable", - "keywords": ["dump", "debug"], - "homepage": "https://symfony.com", - "license": "MIT", - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0" - }, - "require-dev": { - "ext-iconv": "*", - "symfony/console": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^6.3|^7.0", - "symfony/http-kernel": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/uid": "^5.4|^6.0|^7.0", - "twig/twig": "^2.13|^3.0.4" - }, - "conflict": { - "symfony/console": "<5.4" - }, - "autoload": { - "files": [ "Resources/functions/dump.php" ], - "psr-4": { "Symfony\\Component\\VarDumper\\": "" }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "bin": [ - "Resources/bin/var-dump-server" - ], - "minimum-stability": "dev" -} diff --git a/data/web/inc/lib/vendor/tightenco/collect/.github/workflows/run-tests.yml b/data/web/inc/lib/vendor/tightenco/collect/.github/workflows/run-tests.yml deleted file mode 100644 index 406c40049..000000000 --- a/data/web/inc/lib/vendor/tightenco/collect/.github/workflows/run-tests.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Run tests - -on: - push: - branches: [laravel-9-ongoing, laravel-8-ongoing] - pull_request: - -jobs: - tests: - strategy: - matrix: - os: [Ubuntu, macOS] - php: [8.0, 8.1, 8.2] - - include: - - os: Ubuntu - os-version: ubuntu-latest - - - os: macOS - os-version: macos-latest - - name: ${{ matrix.os }} - PHP ${{ matrix.php }} - - runs-on: ${{ matrix.os-version }} - - steps: - - name: Checkout code - uses: actions/checkout@v1 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: posix, dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick - coverage: none - - - name: Install dependencies - run: composer update --prefer-stable --prefer-dist --no-interaction - - - name: Run tests - run: bash upgrade.sh diff --git a/data/web/inc/lib/vendor/tightenco/collect/branch-commit-push.sh b/data/web/inc/lib/vendor/tightenco/collect/branch-commit-push.sh deleted file mode 100755 index 124e58331..000000000 --- a/data/web/inc/lib/vendor/tightenco/collect/branch-commit-push.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/bash - - -GREEN='\033[0;32m' -RED='\033[0;31m' -WHITE='\033[0;37m' -RESET='\033[0m' - -function validateVersion() -{ - echo "" - passedVersion=$1 - echo -e "${WHITE}-- Validating tag '$passedVersion'...${RESET}" - - # Todo: validate the version here using a regex; if fail, just exit - # ... expect 8.75.0, with no v in front of it - - if [[ $passedVersion == '' ]]; then - echo -e "\n-- Invalid tag. Tags should be structured without v; e.g. 8.57.0" - exit - fi - - echo -e "${WHITE}-- Tag valid.${RESET}" - echo "" -} - -# Exit script if any command fails (e.g. phpunit) -set -e - - -# Require confirmation it's set up corrctly -echo -echo -e "${WHITE}-- This script is meant to be run after running upgrade.sh, BEFORE committing to Git.${RESET}" - -while true; do - echo -e "${GREEN}-- Is that the current state of your local project?${RESET}" - read -p "-- (y/n) " yn - case $yn in - [Yy]* ) break;; - [Nn]* ) exit;; - * ) echo "Please answer y or n.";; - esac -done - -# Get the version and exit if not valid -validateVersion $1 - -# Create official v prefaced version -version="v$1" - -# Run tests (and bail if they fail) -phpunit -echo -e "\n${WHITE}-- Tests succeeded.${RESET}" - -# Branch -echo -e "\n${WHITE}-- Creating a Git branch '$version-changes'...${RESET}\n" -git checkout -b $version-changes - -# Add and commit, with "v8.57.0 changes" as the commit name -git add -A -git commit -m "$version changes" - -echo -echo -e "${WHITE}-- Git committed.${RESET}" - -# Push -git push -u origin $version-changes diff --git a/data/web/inc/lib/vendor/tightenco/collect/composer.json b/data/web/inc/lib/vendor/tightenco/collect/composer.json deleted file mode 100644 index 949635dcd..000000000 --- a/data/web/inc/lib/vendor/tightenco/collect/composer.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "name": "tightenco/collect", - "description": "Collect - Illuminate Collections as a separate package.", - "keywords": ["laravel", "collection"], - "license": "MIT", - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com" - } - ], - "require": { - "php": "^8.0", - "symfony/var-dumper": "^3.4 || ^4.0 || ^5.0 || ^6.0" - }, - "require-dev": { - "mockery/mockery": "^1.0", - "phpunit/phpunit": "^8.3", - "nesbot/carbon": "^2.23.0" - }, - "autoload": { - "files": [ - "src/Collect/Support/helpers.php", - "src/Collect/Support/alias.php" - ], - "psr-4": { - "Tightenco\\Collect\\": "src/Collect" - } - }, - "autoload-dev": { - "files": [ - "tests/files/Support/Carbon.php", - "tests/files/Support/HtmlString.php", - "tests/files/Support/HigherOrderTapProxy.php", - "tests/files/Support/Str.php", - "tests/files/Support/Stringable.php", - "tests/files/Support/ItemNotFoundException.php", - "tests/files/Support/MultipleItemsFoundException.php", - "tests/Support/Concerns/CountsEnumerations.php" - ] - }, - "scripts": { - "test": [ - "@composer install", - "phpunit" - ] - }, - "minimum-stability": "dev", - "prefer-stable": true -} diff --git a/data/web/inc/lib/vendor/tightenco/collect/framework-.zip b/data/web/inc/lib/vendor/tightenco/collect/framework-.zip deleted file mode 100644 index e69de29bb..000000000 diff --git a/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Contracts/Support/Arrayable.php b/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Contracts/Support/Arrayable.php deleted file mode 100755 index 190bb9bc3..000000000 --- a/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Contracts/Support/Arrayable.php +++ /dev/null @@ -1,17 +0,0 @@ - - */ - public function toArray(); -} diff --git a/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Contracts/Support/CanBeEscapedWhenCastToString.php b/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Contracts/Support/CanBeEscapedWhenCastToString.php deleted file mode 100644 index 6f3ba0025..000000000 --- a/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Contracts/Support/CanBeEscapedWhenCastToString.php +++ /dev/null @@ -1,14 +0,0 @@ - $needles - * @param bool $ignoreCase - * @return bool - */ - public static function contains($haystack, $needles, $ignoreCase = false) - { - if ($ignoreCase) { - $haystack = mb_strtolower($haystack); - } - - if (! is_iterable($needles)) { - $needles = (array) $needles; - } - - foreach ($needles as $needle) { - if ($ignoreCase) { - $needle = mb_strtolower($needle); - } - - if ($needle !== '' && str_contains($haystack, $needle)) { - return true; - } - } - - return false; - } - - /** - * Determine if a given string contains all array values. - * - * @param string $haystack - * @param iterable $needles - * @param bool $ignoreCase - * @return bool - */ - public static function containsAll($haystack, $needles, $ignoreCase = false) - { - foreach ($needles as $needle) { - if (! static::contains($haystack, $needle, $ignoreCase)) { - return false; - } - } - - return true; - } - - /** - * Determine if a given string ends with a given substring. - * - * @param string $haystack - * @param string|iterable $needles - * @return bool - */ - public static function endsWith($haystack, $needles) - { - if (! is_iterable($needles)) { - $needles = (array) $needles; - } - - foreach ($needles as $needle) { - if ((string) $needle !== '' && str_ends_with($haystack, $needle)) { - return true; - } - } - - return false; - } - - /** - * Extracts an excerpt from text that matches the first instance of a phrase. - * - * @param string $text - * @param string $phrase - * @param array $options - * @return string|null - */ - public static function excerpt($text, $phrase = '', $options = []) - { - $radius = $options['radius'] ?? 100; - $omission = $options['omission'] ?? '...'; - - preg_match('/^(.*?)('.preg_quote((string) $phrase).')(.*)$/iu', (string) $text, $matches); - - if (empty($matches)) { - return null; - } - - $start = ltrim($matches[1]); - - $start = str(mb_substr($start, max(mb_strlen($start, 'UTF-8') - $radius, 0), $radius, 'UTF-8'))->ltrim()->unless( - fn ($startWithRadius) => $startWithRadius->exactly($start), - fn ($startWithRadius) => $startWithRadius->prepend($omission), - ); - - $end = rtrim($matches[3]); - - $end = str(mb_substr($end, 0, $radius, 'UTF-8'))->rtrim()->unless( - fn ($endWithRadius) => $endWithRadius->exactly($end), - fn ($endWithRadius) => $endWithRadius->append($omission), - ); - - return $start->append($matches[2], $end)->toString(); - } - - /** - * Cap a string with a single instance of a given value. - * - * @param string $value - * @param string $cap - * @return string - */ - public static function finish($value, $cap) - { - $quoted = preg_quote($cap, '/'); - - return preg_replace('/(?:'.$quoted.')+$/u', '', $value).$cap; - } - - /** - * Wrap the string with the given strings. - * - * @param string $value - * @param string $before - * @param string|null $after - * @return string - */ - public static function wrap($value, $before, $after = null) - { - return $before.$value.($after ??= $before); - } - - /** - * Determine if a given string matches a given pattern. - * - * @param string|iterable $pattern - * @param string $value - * @return bool - */ - public static function is($pattern, $value) - { - $value = (string) $value; - - if (! is_iterable($pattern)) { - $pattern = [$pattern]; - } - - foreach ($pattern as $pattern) { - $pattern = (string) $pattern; - - // If the given value is an exact match we can of course return true right - // from the beginning. Otherwise, we will translate asterisks and do an - // actual pattern match against the two strings to see if they match. - if ($pattern === $value) { - return true; - } - - $pattern = preg_quote($pattern, '#'); - - // Asterisks are translated into zero-or-more regular expression wildcards - // to make it convenient to check if the strings starts with the given - // pattern such as "library/*", making any string check convenient. - $pattern = str_replace('\*', '.*', $pattern); - - if (preg_match('#^'.$pattern.'\z#u', $value) === 1) { - return true; - } - } - - return false; - } - - /** - * Determine if a given string is 7 bit ASCII. - * - * @param string $value - * @return bool - */ - public static function isAscii($value) - { - return ASCII::is_ascii((string) $value); - } - - /** - * Determine if a given string is valid JSON. - * - * @param string $value - * @return bool - */ - public static function isJson($value) - { - if (! is_string($value)) { - return false; - } - - try { - json_decode($value, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException) { - return false; - } - - return true; - } - - /** - * Determine if a given string is a valid UUID. - * - * @param string $value - * @return bool - */ - public static function isUuid($value) - { - if (! is_string($value)) { - return false; - } - - return preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0; - } - - /** - * Determine if a given string is a valid ULID. - * - * @param string $value - * @return bool - */ - public static function isUlid($value) - { - if (! is_string($value)) { - return false; - } - - return Ulid::isValid($value); - } - - /** - * Convert a string to kebab case. - * - * @param string $value - * @return string - */ - public static function kebab($value) - { - return static::snake($value, '-'); - } - - /** - * Return the length of the given string. - * - * @param string $value - * @param string|null $encoding - * @return int - */ - public static function length($value, $encoding = null) - { - if ($encoding) { - return mb_strlen($value, $encoding); - } - - return mb_strlen($value); - } - - /** - * Limit the number of characters in a string. - * - * @param string $value - * @param int $limit - * @param string $end - * @return string - */ - public static function limit($value, $limit = 100, $end = '...') - { - if (mb_strwidth($value, 'UTF-8') <= $limit) { - return $value; - } - - return rtrim(mb_strimwidth($value, 0, $limit, '', 'UTF-8')).$end; - } - - /** - * Convert the given string to lower-case. - * - * @param string $value - * @return string - */ - public static function lower($value) - { - return mb_strtolower($value, 'UTF-8'); - } - - /** - * Limit the number of words in a string. - * - * @param string $value - * @param int $words - * @param string $end - * @return string - */ - public static function words($value, $words = 100, $end = '...') - { - preg_match('/^\s*+(?:\S++\s*+){1,'.$words.'}/u', $value, $matches); - - if (! isset($matches[0]) || static::length($value) === static::length($matches[0])) { - return $value; - } - - return rtrim($matches[0]).$end; - } - - /** - * Converts GitHub flavored Markdown into HTML. - * - * @param string $string - * @param array $options - * @return string - */ - public static function markdown($string, array $options = []) - { - $converter = new GithubFlavoredMarkdownConverter($options); - - return (string) $converter->convert($string); - } - - /** - * Converts inline Markdown into HTML. - * - * @param string $string - * @param array $options - * @return string - */ - public static function inlineMarkdown($string, array $options = []) - { - $environment = new Environment($options); - - $environment->addExtension(new GithubFlavoredMarkdownExtension()); - $environment->addExtension(new InlinesOnlyExtension()); - - $converter = new MarkdownConverter($environment); - - return (string) $converter->convert($string); - } - - /** - * Masks a portion of a string with a repeated character. - * - * @param string $string - * @param string $character - * @param int $index - * @param int|null $length - * @param string $encoding - * @return string - */ - public static function mask($string, $character, $index, $length = null, $encoding = 'UTF-8') - { - if ($character === '') { - return $string; - } - - $segment = mb_substr($string, $index, $length, $encoding); - - if ($segment === '') { - return $string; - } - - $strlen = mb_strlen($string, $encoding); - $startIndex = $index; - - if ($index < 0) { - $startIndex = $index < -$strlen ? 0 : $strlen + $index; - } - - $start = mb_substr($string, 0, $startIndex, $encoding); - $segmentLen = mb_strlen($segment, $encoding); - $end = mb_substr($string, $startIndex + $segmentLen); - - return $start.str_repeat(mb_substr($character, 0, 1, $encoding), $segmentLen).$end; - } - - /** - * Get the string matching the given pattern. - * - * @param string $pattern - * @param string $subject - * @return string - */ - public static function match($pattern, $subject) - { - preg_match($pattern, $subject, $matches); - - if (! $matches) { - return ''; - } - - return $matches[1] ?? $matches[0]; - } - - /** - * Get the string matching the given pattern. - * - * @param string $pattern - * @param string $subject - * @return \Tightenco\Collect\Support\Collection - */ - public static function matchAll($pattern, $subject) - { - preg_match_all($pattern, $subject, $matches); - - if (empty($matches[0])) { - return collect(); - } - - return collect($matches[1] ?? $matches[0]); - } - - /** - * Pad both sides of a string with another. - * - * @param string $value - * @param int $length - * @param string $pad - * @return string - */ - public static function padBoth($value, $length, $pad = ' ') - { - $short = max(0, $length - mb_strlen($value)); - $shortLeft = floor($short / 2); - $shortRight = ceil($short / 2); - - return mb_substr(str_repeat($pad, $shortLeft), 0, $shortLeft). - $value. - mb_substr(str_repeat($pad, $shortRight), 0, $shortRight); - } - - /** - * Pad the left side of a string with another. - * - * @param string $value - * @param int $length - * @param string $pad - * @return string - */ - public static function padLeft($value, $length, $pad = ' ') - { - $short = max(0, $length - mb_strlen($value)); - - return mb_substr(str_repeat($pad, $short), 0, $short).$value; - } - - /** - * Pad the right side of a string with another. - * - * @param string $value - * @param int $length - * @param string $pad - * @return string - */ - public static function padRight($value, $length, $pad = ' ') - { - $short = max(0, $length - mb_strlen($value)); - - return $value.mb_substr(str_repeat($pad, $short), 0, $short); - } - - /** - * Parse a Class[@]method style callback into class and method. - * - * @param string $callback - * @param string|null $default - * @return array - */ - public static function parseCallback($callback, $default = null) - { - return static::contains($callback, '@') ? explode('@', $callback, 2) : [$callback, $default]; - } - - /** - * Get the plural form of an English word. - * - * @param string $value - * @param int|array|\Countable $count - * @return string - */ - public static function plural($value, $count = 2) - { - return Pluralizer::plural($value, $count); - } - - /** - * Pluralize the last word of an English, studly caps case string. - * - * @param string $value - * @param int|array|\Countable $count - * @return string - */ - public static function pluralStudly($value, $count = 2) - { - $parts = preg_split('/(.)(?=[A-Z])/u', $value, -1, PREG_SPLIT_DELIM_CAPTURE); - - $lastWord = array_pop($parts); - - return implode('', $parts).self::plural($lastWord, $count); - } - - /** - * Generate a more truly "random" alpha-numeric string. - * - * @param int $length - * @return string - */ - public static function random($length = 16) - { - return (static::$randomStringFactory ?? function ($length) { - $string = ''; - - while (($len = strlen($string)) < $length) { - $size = $length - $len; - - $bytesSize = (int) ceil($size / 3) * 3; - - $bytes = random_bytes($bytesSize); - - $string .= substr(str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size); - } - - return $string; - })($length); - } - - /** - * Set the callable that will be used to generate random strings. - * - * @param callable|null $factory - * @return void - */ - public static function createRandomStringsUsing(callable $factory = null) - { - static::$randomStringFactory = $factory; - } - - /** - * Set the sequence that will be used to generate random strings. - * - * @param array $sequence - * @param callable|null $whenMissing - * @return void - */ - public static function createRandomStringsUsingSequence(array $sequence, $whenMissing = null) - { - $next = 0; - - $whenMissing ??= function ($length) use (&$next) { - $factoryCache = static::$randomStringFactory; - - static::$randomStringFactory = null; - - $randomString = static::random($length); - - static::$randomStringFactory = $factoryCache; - - $next++; - - return $randomString; - }; - - static::createRandomStringsUsing(function ($length) use (&$next, $sequence, $whenMissing) { - if (array_key_exists($next, $sequence)) { - return $sequence[$next++]; - } - - return $whenMissing($length); - }); - } - - /** - * Indicate that random strings should be created normally and not using a custom factory. - * - * @return void - */ - public static function createRandomStringsNormally() - { - static::$randomStringFactory = null; - } - - /** - * Repeat the given string. - * - * @param string $string - * @param int $times - * @return string - */ - public static function repeat(string $string, int $times) - { - return str_repeat($string, $times); - } - - /** - * Replace a given value in the string sequentially with an array. - * - * @param string $search - * @param iterable $replace - * @param string $subject - * @return string - */ - public static function replaceArray($search, $replace, $subject) - { - if ($replace instanceof Traversable) { - $replace = collect($replace)->all(); - } - - $segments = explode($search, $subject); - - $result = array_shift($segments); - - foreach ($segments as $segment) { - $result .= (array_shift($replace) ?? $search).$segment; - } - - return $result; - } - - /** - * Replace the given value in the given string. - * - * @param string|iterable $search - * @param string|iterable $replace - * @param string|iterable $subject - * @return string - */ - public static function replace($search, $replace, $subject) - { - if ($search instanceof Traversable) { - $search = collect($search)->all(); - } - - if ($replace instanceof Traversable) { - $replace = collect($replace)->all(); - } - - if ($subject instanceof Traversable) { - $subject = collect($subject)->all(); - } - - return str_replace($search, $replace, $subject); - } - - /** - * Replace the first occurrence of a given value in the string. - * - * @param string $search - * @param string $replace - * @param string $subject - * @return string - */ - public static function replaceFirst($search, $replace, $subject) - { - $search = (string) $search; - - if ($search === '') { - return $subject; - } - - $position = strpos($subject, $search); - - if ($position !== false) { - return substr_replace($subject, $replace, $position, strlen($search)); - } - - return $subject; - } - - /** - * Replace the last occurrence of a given value in the string. - * - * @param string $search - * @param string $replace - * @param string $subject - * @return string - */ - public static function replaceLast($search, $replace, $subject) - { - if ($search === '') { - return $subject; - } - - $position = strrpos($subject, $search); - - if ($position !== false) { - return substr_replace($subject, $replace, $position, strlen($search)); - } - - return $subject; - } - - /** - * Remove any occurrence of the given string in the subject. - * - * @param string|iterable $search - * @param string $subject - * @param bool $caseSensitive - * @return string - */ - public static function remove($search, $subject, $caseSensitive = true) - { - if ($search instanceof Traversable) { - $search = collect($search)->all(); - } - - $subject = $caseSensitive - ? str_replace($search, '', $subject) - : str_ireplace($search, '', $subject); - - return $subject; - } - - /** - * Reverse the given string. - * - * @param string $value - * @return string - */ - public static function reverse(string $value) - { - return implode(array_reverse(mb_str_split($value))); - } - - /** - * Begin a string with a single instance of a given value. - * - * @param string $value - * @param string $prefix - * @return string - */ - public static function start($value, $prefix) - { - $quoted = preg_quote($prefix, '/'); - - return $prefix.preg_replace('/^(?:'.$quoted.')+/u', '', $value); - } - - /** - * Convert the given string to upper-case. - * - * @param string $value - * @return string - */ - public static function upper($value) - { - return mb_strtoupper($value, 'UTF-8'); - } - - /** - * Convert the given string to title case. - * - * @param string $value - * @return string - */ - public static function title($value) - { - return mb_convert_case($value, MB_CASE_TITLE, 'UTF-8'); - } - - /** - * Convert the given string to title case for each word. - * - * @param string $value - * @return string - */ - public static function headline($value) - { - $parts = explode(' ', $value); - - $parts = count($parts) > 1 - ? array_map([static::class, 'title'], $parts) - : array_map([static::class, 'title'], static::ucsplit(implode('_', $parts))); - - $collapsed = static::replace(['-', '_', ' '], '_', implode('_', $parts)); - - return implode(' ', array_filter(explode('_', $collapsed))); - } - - /** - * Get the singular form of an English word. - * - * @param string $value - * @return string - */ - public static function singular($value) - { - return Pluralizer::singular($value); - } - - /** - * Generate a URL friendly "slug" from a given string. - * - * @param string $title - * @param string $separator - * @param string|null $language - * @param array $dictionary - * @return string - */ - public static function slug($title, $separator = '-', $language = 'en', $dictionary = ['@' => 'at']) - { - $title = $language ? static::ascii($title, $language) : $title; - - // Convert all dashes/underscores into separator - $flip = $separator === '-' ? '_' : '-'; - - $title = preg_replace('!['.preg_quote($flip).']+!u', $separator, $title); - - // Replace dictionary words - foreach ($dictionary as $key => $value) { - $dictionary[$key] = $separator.$value.$separator; - } - - $title = str_replace(array_keys($dictionary), array_values($dictionary), $title); - - // Remove all characters that are not the separator, letters, numbers, or whitespace - $title = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', static::lower($title)); - - // Replace all separator characters and whitespace by a single separator - $title = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $title); - - return trim($title, $separator); - } - - /** - * Convert a string to snake case. - * - * @param string $value - * @param string $delimiter - * @return string - */ - public static function snake($value, $delimiter = '_') - { - $key = $value; - - if (isset(static::$snakeCache[$key][$delimiter])) { - return static::$snakeCache[$key][$delimiter]; - } - - if (! ctype_lower($value)) { - $value = preg_replace('/\s+/u', '', ucwords($value)); - - $value = static::lower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $value)); - } - - return static::$snakeCache[$key][$delimiter] = $value; - } - - /** - * Remove all "extra" blank space from the given string. - * - * @param string $value - * @return string - */ - public static function squish($value) - { - return preg_replace('~(\s|\x{3164})+~u', ' ', preg_replace('~^[\s\x{FEFF}]+|[\s\x{FEFF}]+$~u', '', $value)); - } - - /** - * Determine if a given string starts with a given substring. - * - * @param string $haystack - * @param string|iterable $needles - * @return bool - */ - public static function startsWith($haystack, $needles) - { - if (! is_iterable($needles)) { - $needles = [$needles]; - } - - foreach ($needles as $needle) { - if ((string) $needle !== '' && str_starts_with($haystack, $needle)) { - return true; - } - } - - return false; - } - - /** - * Convert a value to studly caps case. - * - * @param string $value - * @return string - */ - public static function studly($value) - { - $key = $value; - - if (isset(static::$studlyCache[$key])) { - return static::$studlyCache[$key]; - } - - $words = explode(' ', static::replace(['-', '_'], ' ', $value)); - - $studlyWords = array_map(fn ($word) => static::ucfirst($word), $words); - - return static::$studlyCache[$key] = implode($studlyWords); - } - - /** - * Returns the portion of the string specified by the start and length parameters. - * - * @param string $string - * @param int $start - * @param int|null $length - * @param string $encoding - * @return string - */ - public static function substr($string, $start, $length = null, $encoding = 'UTF-8') - { - return mb_substr($string, $start, $length, $encoding); - } - - /** - * Returns the number of substring occurrences. - * - * @param string $haystack - * @param string $needle - * @param int $offset - * @param int|null $length - * @return int - */ - public static function substrCount($haystack, $needle, $offset = 0, $length = null) - { - if (! is_null($length)) { - return substr_count($haystack, $needle, $offset, $length); - } - - return substr_count($haystack, $needle, $offset); - } - - /** - * Replace text within a portion of a string. - * - * @param string|string[] $string - * @param string|string[] $replace - * @param int|int[] $offset - * @param int|int[]|null $length - * @return string|string[] - */ - public static function substrReplace($string, $replace, $offset = 0, $length = null) - { - if ($length === null) { - $length = strlen($string); - } - - return substr_replace($string, $replace, $offset, $length); - } - - /** - * Swap multiple keywords in a string with other keywords. - * - * @param array $map - * @param string $subject - * @return string - */ - public static function swap(array $map, $subject) - { - return strtr($subject, $map); - } - - /** - * Make a string's first character lowercase. - * - * @param string $string - * @return string - */ - public static function lcfirst($string) - { - return static::lower(static::substr($string, 0, 1)).static::substr($string, 1); - } - - /** - * Make a string's first character uppercase. - * - * @param string $string - * @return string - */ - public static function ucfirst($string) - { - return static::upper(static::substr($string, 0, 1)).static::substr($string, 1); - } - - /** - * Split a string into pieces by uppercase characters. - * - * @param string $string - * @return string[] - */ - public static function ucsplit($string) - { - return preg_split('/(?=\p{Lu})/u', $string, -1, PREG_SPLIT_NO_EMPTY); - } - - /** - * Get the number of words a string contains. - * - * @param string $string - * @param string|null $characters - * @return int - */ - public static function wordCount($string, $characters = null) - { - return str_word_count($string, 0, $characters); - } - - /** - * Generate a UUID (version 4). - * - * @return \Ramsey\Uuid\UuidInterface - */ - public static function uuid() - { - return static::$uuidFactory - ? call_user_func(static::$uuidFactory) - : Uuid::uuid4(); - } - - /** - * Generate a time-ordered UUID (version 4). - * - * @return \Ramsey\Uuid\UuidInterface - */ - public static function orderedUuid() - { - if (static::$uuidFactory) { - return call_user_func(static::$uuidFactory); - } - - $factory = new UuidFactory; - - $factory->setRandomGenerator(new CombGenerator( - $factory->getRandomGenerator(), - $factory->getNumberConverter() - )); - - $factory->setCodec(new TimestampFirstCombCodec( - $factory->getUuidBuilder() - )); - - return $factory->uuid4(); - } - - /** - * Set the callable that will be used to generate UUIDs. - * - * @param callable|null $factory - * @return void - */ - public static function createUuidsUsing(callable $factory = null) - { - static::$uuidFactory = $factory; - } - - /** - * Set the sequence that will be used to generate UUIDs. - * - * @param array $sequence - * @param callable|null $whenMissing - * @return void - */ - public static function createUuidsUsingSequence(array $sequence, $whenMissing = null) - { - $next = 0; - - $whenMissing ??= function () use (&$next) { - $factoryCache = static::$uuidFactory; - - static::$uuidFactory = null; - - $uuid = static::uuid(); - - static::$uuidFactory = $factoryCache; - - $next++; - - return $uuid; - }; - - static::createUuidsUsing(function () use (&$next, $sequence, $whenMissing) { - if (array_key_exists($next, $sequence)) { - return $sequence[$next++]; - } - - return $whenMissing(); - }); - } - - /** - * Always return the same UUID when generating new UUIDs. - * - * @param \Closure|null $callback - * @return \Ramsey\Uuid\UuidInterface - */ - public static function freezeUuids(Closure $callback = null) - { - $uuid = Str::uuid(); - - Str::createUuidsUsing(fn () => $uuid); - - if ($callback !== null) { - try { - $callback($uuid); - } finally { - Str::createUuidsNormally(); - } - } - - return $uuid; - } - - /** - * Indicate that UUIDs should be created normally and not using a custom factory. - * - * @return void - */ - public static function createUuidsNormally() - { - static::$uuidFactory = null; - } - - /** - * Generate a ULID. - * - * @return \Symfony\Component\Uid\Ulid - */ - public static function ulid() - { - return new Ulid(); - } - - /** - * Remove all strings from the casing caches. - * - * @return void - */ - public static function flushCache() - { - static::$snakeCache = []; - static::$camelCache = []; - static::$studlyCache = []; - } -} diff --git a/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Support/Traits/Tappable.php b/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Support/Traits/Tappable.php deleted file mode 100644 index 9d75d26fb..000000000 --- a/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Support/Traits/Tappable.php +++ /dev/null @@ -1,17 +0,0 @@ - Illuminate\Contracts\Support\Arrayable::class, - Tightenco\Collect\Contracts\Support\Jsonable::class => Illuminate\Contracts\Support\Jsonable::class, - Tightenco\Collect\Contracts\Support\Htmlable::class => Illuminate\Contracts\Support\Htmlable::class, - Tightenco\Collect\Contracts\Support\CanBeEscapedWhenCastToString::class => Illuminate\Contracts\Support\CanBeEscapedWhenCastToString::class, - Tightenco\Collect\Support\Arr::class => Illuminate\Support\Arr::class, - Tightenco\Collect\Support\Collection::class => Illuminate\Support\Collection::class, - Tightenco\Collect\Support\Enumerable::class => Illuminate\Support\Enumerable::class, - Tightenco\Collect\Support\HigherOrderCollectionProxy::class => Illuminate\Support\HigherOrderCollectionProxy::class, - Tightenco\Collect\Support\LazyCollection::class => Illuminate\Support\LazyCollection::class, - Tightenco\Collect\Support\Traits\EnumeratesValues::class => Illuminate\Support\Traits\EnumeratesValues::class, -]; - -# echo "\n\n-- Aliasing....\n---------------------------------------------\n\n"; - -foreach ($aliases as $tighten => $illuminate) { - if (! class_exists($illuminate) && ! interface_exists($illuminate) && ! trait_exists($illuminate)) { - # echo "Aliasing {$tighten} to {$illuminate}.\n"; - class_alias($tighten, $illuminate); - } -} diff --git a/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Support/helpers.php b/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Support/helpers.php deleted file mode 100644 index 886a1414c..000000000 --- a/data/web/inc/lib/vendor/tightenco/collect/src/Collect/Support/helpers.php +++ /dev/null @@ -1,122 +0,0 @@ - $segment) { - unset($key[$i]); - - if (is_null($segment)) { - return $target; - } - - if ($segment === '*') { - if ($target instanceof Collection) { - $target = $target->all(); - } elseif (! is_array($target)) { - return value($default); - } - - $result = []; - - foreach ($target as $item) { - $result[] = data_get($item, $key); - } - - return in_array('*', $key) ? Arr::collapse($result) : $result; - } - - if (Arr::accessible($target) && Arr::exists($target, $segment)) { - $target = $target[$segment]; - } elseif (is_object($target) && isset($target->{$segment})) { - $target = $target->{$segment}; - } else { - return value($default); - } - } - - return $target; - } - } - - if (! function_exists('tap')) { - /** - * Call the given Closure with the given value then return the value. - * - * @param mixed $value - * @param callable|null $callback - * @return mixed - */ - function tap($value, $callback = null) - { - if (is_null($callback)) { - return new HigherOrderTapProxy($value); - } - - $callback($value); - - return $value; - } - } - - if (! function_exists('class_basename')) { - /** - * Get the class "basename" of the given object / class. - * - * @param string|object $class - * @return string - */ - function class_basename($class) - { - $class = is_object($class) ? get_class($class) : $class; - - return basename(str_replace('\\', '/', $class)); - } - } -} diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 82671540b..b7ba156cd 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -47,7 +47,7 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/CSSminifierExtended.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/array_merge_real.php'; // T/HOTP API -$qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider(); +$qrprovider = new RobThree\Auth\Providers\Qr\BaconQrCodeProvider(); $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider); // FIDO2 @@ -105,11 +105,11 @@ http_response_code(500); -
    Connection to controller container failed.

    The following error was reported:
    -
    +
    Connection to dockerapi container failed.

    The following error was reported:
    -
    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'); diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index 3dd198451..ac308b687 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -70,7 +70,7 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) { } else { $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); - error_log("mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); + error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); http_response_code(401); echo json_encode(array( 'type' => 'error', @@ -82,7 +82,7 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) { } else { $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); - error_log("mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); + error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); http_response_code(401); echo json_encode(array( 'type' => 'error', @@ -92,16 +92,6 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) { exit(); } } -else { - $remote = get_remote_ip(false); - $docker_ipv4_network = getenv('IPV4_NETWORK'); - if ($remote == "{$docker_ipv4_network}.246") { - $_SESSION['mailcow_cc_username'] = 'Controller'; - $_SESSION['mailcow_cc_role'] = 'admin'; - $_SESSION['mailcow_cc_api'] = true; - $_SESSION['mailcow_cc_api_access'] = 'rw'; - } -} // Handle logouts if (isset($_POST["logout"])) { @@ -150,17 +140,32 @@ function session_check() { ); return false; } - if (!empty($_POST)) { - if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) { - $_SESSION['return'][] = array( - 'type' => 'warning', - 'msg' => 'session_token' - ); - return false; + // Check if this is a POST request (form-encoded or JSON) + $is_post_request = !empty($_POST) || ( + isset($_SERVER['CONTENT_TYPE']) && + strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false + ); + + if ($is_post_request) { + // Skip CSRF check for DataTables server-side processing endpoints + // These are read-only operations (equivalent to GET) authenticated by session + $is_search_endpoint = ( + isset($_GET['query']) && + preg_match('#^search/(domain|mailbox)$#', $_GET['query']) + ); + + if (!$is_search_endpoint && !empty($_POST)) { + if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) { + $_SESSION['return'][] = array( + 'type' => 'warning', + 'msg' => 'session_token' + ); + return false; + } + unset($_POST['csrf_token']); + $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32)); + $_SESSION['CSRF']['TIME'] = time(); } - unset($_POST['csrf_token']); - $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32)); - $_SESSION['CSRF']['TIME'] = time(); } return true; } diff --git a/data/web/inc/triggers.admin.inc.php b/data/web/inc/triggers.admin.inc.php index 2a02ba511..92043190e 100644 --- a/data/web/inc/triggers.admin.inc.php +++ b/data/web/inc/triggers.admin.inc.php @@ -9,6 +9,11 @@ if (isset($_POST["verify_tfa_login"])) { unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_methods']); + // If pending actions exist, redirect to /admin to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /admin"); + die(); + } header("Location: /admin/dashboard"); die(); } @@ -42,6 +47,15 @@ if (isset($_GET["cancel_tfa_login"])) { header("Location: /admin"); } +if (isset($_GET["cancel_tfa_setup"])) { + session_regenerate_id(true); + session_unset(); + session_destroy(); + session_write_close(); + header("Location: /admin"); + exit(); +} + if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { $login_user = strtolower(trim($_POST["login_user"])); $as = check_login($login_user, $_POST["pass_user"], array("role" => "admin", "service" => "MAILCOWUI")); @@ -50,6 +64,11 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { session_regenerate_id(true); $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "admin"; + // If pending actions exist, redirect to /admin to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /admin"); + die(); + } header("Location: /admin/dashboard"); die(); } diff --git a/data/web/inc/triggers.domainadmin.inc.php b/data/web/inc/triggers.domainadmin.inc.php index 764d9009b..2eee8b993 100644 --- a/data/web/inc/triggers.domainadmin.inc.php +++ b/data/web/inc/triggers.domainadmin.inc.php @@ -20,6 +20,11 @@ if (isset($_POST["verify_tfa_login"])) { unset($_SESSION['pending_mailcow_cc_role']); unset($_SESSION['pending_tfa_methods']); + // If pending actions exist, redirect to /domainadmin to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /domainadmin"); + die(); + } header("Location: /domainadmin/mailbox"); die(); } @@ -53,6 +58,15 @@ if (isset($_GET["cancel_tfa_login"])) { header("Location: /domainadmin"); } +if (isset($_GET["cancel_tfa_setup"])) { + session_regenerate_id(true); + session_unset(); + session_destroy(); + session_write_close(); + header("Location: /domainadmin"); + exit(); +} + if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { $login_user = strtolower(trim($_POST["login_user"])); $as = check_login($login_user, $_POST["pass_user"], array("role" => "domain_admin", "service" => "MAILCOWUI")); @@ -61,6 +75,11 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { session_regenerate_id(true); $_SESSION['mailcow_cc_username'] = $login_user; $_SESSION['mailcow_cc_role'] = "domainadmin"; + // If pending actions exist, redirect to /domainadmin to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /domainadmin"); + die(); + } header("Location: /domainadmin/mailbox"); die(); } diff --git a/data/web/inc/triggers.global.inc.php b/data/web/inc/triggers.global.inc.php index dd88fad56..1f8f92004 100644 --- a/data/web/inc/triggers.global.inc.php +++ b/data/web/inc/triggers.global.inc.php @@ -36,7 +36,26 @@ if (isset($_SESSION['mailcow_cc_role']) && (isset($_SESSION['acl']['login_as']) if (isset($_SESSION['mailcow_cc_role'])) { if (isset($_POST["set_tfa"])) { + $had_pending_tfa_setup = !empty($_SESSION['pending_tfa_setup']); set_tfa($_POST); + // After TFA setup during forced enrollment + if ($had_pending_tfa_setup && empty($_SESSION['pending_tfa_setup'])) { + if ($_SESSION['mailcow_cc_role'] === 'admin') { + header("Location: /admin/dashboard"); + } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') { + header("Location: /domainadmin/mailbox"); + } elseif ($_SESSION['mailcow_cc_role'] === 'user') { + // Check if user should go to SOGo or /user + $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); + $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual && getenv('SKIP_SOGO') != "y") { + header("Location: /SOGo/so/"); + } else { + header("Location: /user"); + } + } + exit(); + } } if (isset($_POST["unset_tfa_key"])) { unset_tfa_key($_POST); diff --git a/data/web/inc/triggers.user.inc.php b/data/web/inc/triggers.user.inc.php index cc33596f9..5dcde0b18 100644 --- a/data/web/inc/triggers.user.inc.php +++ b/data/web/inc/triggers.user.inc.php @@ -76,6 +76,11 @@ if (isset($_POST["verify_tfa_login"])) { $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + // If pending actions exist, redirect to / to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /"); + die(); + } if (intval($user_details['attributes']['sogo_access']) == 1 && intval($user_details['attributes']['force_pw_update']) != 1 && getenv('SKIP_SOGO') != "y" && @@ -117,6 +122,15 @@ if (isset($_GET["cancel_tfa_login"])) { header("Location: /"); } +if (isset($_GET["cancel_tfa_setup"])) { + session_regenerate_id(true); + session_unset(); + session_destroy(); + session_write_close(); + header("Location: /"); + exit(); +} + if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { $login_user = strtolower(trim($_POST["login_user"])); $as = check_login($login_user, $_POST["pass_user"], array("role" => "user", "service" => "MAILCOWUI")); @@ -142,6 +156,11 @@ if (isset($_POST["login_user"]) && isset($_POST["pass_user"])) { $user_details = mailbox("get", "mailbox_details", $login_user); $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + // If pending actions exist, redirect to / to show modal + if (!empty($_SESSION['pending_tfa_setup']) || !empty($_SESSION['pending_pw_update'])) { + header("Location: /"); + die(); + } if (intval($user_details['attributes']['sogo_access']) == 1 && intval($user_details['attributes']['force_pw_update']) != 1 && getenv('SKIP_SOGO') != "y" && diff --git a/data/web/inc/twig.inc.php b/data/web/inc/twig.inc.php index a3bc02d92..c4d0669d9 100644 --- a/data/web/inc/twig.inc.php +++ b/data/web/inc/twig.inc.php @@ -13,7 +13,9 @@ $twig = new Environment($loader, [ // functions $twig->addFunction(new TwigFunction('query_string', function (array $params = []) { - return http_build_query(array_merge($_GET, $params)); + $allowed = ['lang', 'mobileconfig']; + $filtered = array_intersect_key($_GET, array_flip($allowed)); + return http_build_query(array_merge($filtered, $params)); })); $twig->addFunction(new TwigFunction('is_uri', function (string $uri, string $where = null) { diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 6d1965542..72c5da67a 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -112,6 +112,7 @@ $AVAILABLE_LANGUAGES = array( 'sv-se' => 'Svenska (Swedish)', 'tr-tr' => 'Türkçe (Turkish)', 'uk-ua' => 'Українська (Ukrainian)', + 'uz-uz' => 'Oʻzbekcha (Uzbek)', 'vi-vn' => 'Tiếng Việt (Vietnamese)', 'zh-cn' => '简体中文 (Simplified Chinese)', 'zh-tw' => '繁體中文 (Traditional Chinese)', @@ -120,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 @@ -193,6 +196,9 @@ $MAILBOX_DEFAULT_ATTRIBUTES['tls_enforce_out'] = false; // Force password change on next login (only allows login to mailcow UI) $MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = false; +// Force 2FA enrollment at next login +$MAILBOX_DEFAULT_ATTRIBUTES['force_tfa'] = false; + // Enable SOGo access - Users will be redirected to SOGo after login (set to false to disable redirect by default) $MAILBOX_DEFAULT_ATTRIBUTES['sogo_access'] = true; @@ -242,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 diff --git a/data/web/index.php b/data/web/index.php index a1ff9310f..b037c14f1 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -9,22 +9,28 @@ if (isset($_SESSION['mailcow_cc_role']) && isset($_SESSION['oauth2_request'])) { exit(); } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'user') { - $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); - $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; - if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual && getenv('SKIP_SOGO') != "y") { - header("Location: /SOGo/so/"); - } else { - header("Location: /user"); + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + $user_details = mailbox("get", "mailbox_details", $_SESSION['mailcow_cc_username']); + $is_dual = (!empty($_SESSION["dual-login"]["username"])) ? true : false; + if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual && getenv('SKIP_SOGO') != "y") { + header("Location: /SOGo/so/"); + } else { + header("Location: /user"); + } + exit(); } - exit(); } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'admin') { - header('Location: /admin/dashboard'); - exit(); + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + header('Location: /admin/dashboard'); + exit(); + } } elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'domainadmin') { - header('Location: /domainadmin/mailbox'); - exit(); + if (empty($_SESSION['pending_tfa_setup']) && empty($_SESSION['pending_pw_update'])) { + header('Location: /domainadmin/mailbox'); + exit(); + } } $host = strtolower($_SERVER['HTTP_HOST'] ?? ''); diff --git a/data/web/js/build/001-bootstrap.bundle.min.js b/data/web/js/build/001-bootstrap.bundle.min.js index cc0a25561..0b873693a 100644 --- a/data/web/js/build/001-bootstrap.bundle.min.js +++ b/data/web/js/build/001-bootstrap.bundle.min.js @@ -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;sfunction(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(!l||!l[r])return;return void I(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach((i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach((o=>{if(o.includes(n)){const n=s[o];I(t,e,i,n.originalHandler,n.delegationSelector)}}))}(t,l,i,e.slice(1))}));const h=l[r]||{};Object.keys(h).forEach((i=>{const n=i.replace(E,"");if(!a||e.includes(n)){const e=h[i];I(t,l,r,e.originalHandler,e.delegationSelector)}}))},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f(),s=P(e),o=e!==s,r=k.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach((t=>{Object.defineProperty(d,t,{get:()=>i[t]})})),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};class B{constructor(t){(t=r(t))&&(this._element=t,H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((t=>{this[t]=null}))}_queueCallback(t,e,i=!0){b(t,e,i)}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.3"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}}const R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class W extends B{static get NAME(){return"alert"}close(){if(j.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(W,"close"),g(W);const $='[data-bs-toggle="button"]';class z extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function q(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}j.on(document,"click.bs.button.data-api",$,(t=>{t.preventDefault();const e=t.target.closest($);z.getOrCreateInstance(e).toggle()})),g(z);const U={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter((t=>t.startsWith("bs"))).forEach((i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=q(t.dataset[i])})),e},getDataAttribute:(t,e)=>q(t.getAttribute(`data-bs-${F(e)}`)),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(", ");return this.find(e,t).filter((t=>!c(t)&&l(t)))}},K="carousel",X={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Y={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Q="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z},et="slid.bs.carousel",it="active",nt=".active.carousel-item";class st extends B{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return X}static get NAME(){return K}next(){this._slide(Q)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(s(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=V.findOne(nt,this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,et,(()=>this.to(t)));if(e===t)return this.pause(),void this.cycle();const i=t>e?Q:G;this._slide(i,this._items[t])}_getConfig(t){return t={...X,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(K,t,Y),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&j.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,"mouseenter.bs.carousel",(t=>this.pause(t))),j.on(this._element,"mouseleave.bs.carousel",(t=>this.cycle(t)))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>this._pointerEvent&&("pen"===t.pointerType||"touch"===t.pointerType),e=e=>{t(e)?this.touchStartX=e.clientX:this._pointerEvent||(this.touchStartX=e.touches[0].clientX)},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=e=>{t(e)&&(this.touchDeltaX=e.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((t=>this.cycle(t)),500+this._config.interval))};V.find(".carousel-item img",this._element).forEach((t=>{j.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()))})),this._pointerEvent?(j.on(this._element,"pointerdown.bs.carousel",(t=>e(t))),j.on(this._element,"pointerup.bs.carousel",(t=>n(t))),this._element.classList.add("pointer-event")):(j.on(this._element,"touchstart.bs.carousel",(t=>e(t))),j.on(this._element,"touchmove.bs.carousel",(t=>i(t))),j.on(this._element,"touchend.bs.carousel",(t=>n(t))))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===Q;return v(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),n=this._getItemIndex(V.findOne(nt,this._element));return j.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=V.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=V.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{j.trigger(this._element,et,{relatedTarget:o,direction:d,from:s,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),u(o),n.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add(it),n.classList.remove(it,h,c),this._isSliding=!1,setTimeout(f,0)};this._queueCallback(t,n,!0)}else n.classList.remove(it),o.classList.add(it),this._isSliding=!1,f();a&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?m()?t===Z?G:Q:t===Z?Q:G:t}_orderToDirection(t){return[Q,G].includes(t)?m()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const i=st.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){st.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=n(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),st.carouselInterface(e,i),s&&st.getInstance(e).to(s),t.preventDefault()}}j.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",st.dataApiClickHandler),j.on(window,"load.bs.carousel.data-api",(()=>{const t=V.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element));null!==s&&o.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return rt}static get NAME(){return ot}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=V.find(ut,this._config.parent);e=V.find(".collapse.show, .collapse.collapsing",this._config.parent).filter((e=>!t.includes(e)))}const i=V.findOne(this._selector);if(e.length){const n=e.find((t=>i!==t));if(t=n?pt.getInstance(n):null,t&&t._isTransitioning)return}if(j.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach((e=>{i!==e&&pt.getOrCreateInstance(e,{toggle:!1}).hide(),t||H.set(e,"bs.collapse",null)}));const n=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[n]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const s=`scroll${n[0].toUpperCase()+n.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[n]="",j.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[n]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),j.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_getConfig(t){return(t={...rt,...U.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=r(t.parent),a(ot,t,at),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=V.find(ut,this._config.parent);V.find(ft,this._config.parent).filter((e=>!t.includes(e))).forEach((t=>{const e=n(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}))}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach((t=>{e?t.classList.remove(dt):t.classList.add(dt),t.setAttribute("aria-expanded",e)}))}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,"click.bs.collapse.data-api",ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this);V.find(e).forEach((t=>{pt.getOrCreateInstance(t,{toggle:!1}).toggle()}))})),g(pt);var mt="top",gt="bottom",_t="right",bt="left",vt="auto",yt=[mt,gt,_t,bt],wt="start",Et="end",At="clippingParents",Tt="viewport",Ot="popper",Ct="reference",kt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+Et])}),[]),Lt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+Et])}),[]),xt="beforeRead",Dt="read",St="afterRead",Nt="beforeMain",It="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",Bt=[xt,Dt,St,Nt,It,Pt,jt,Mt,Ht];function Rt(t){return t?(t.nodeName||"").toLowerCase():null}function Wt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function $t(t){return t instanceof Wt(t).Element||t instanceof Element}function zt(t){return t instanceof Wt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Wt(t).ShadowRoot||t instanceof ShadowRoot)}const Ft={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Rt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Rt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Ut(t){return t.split("-")[0]}function Vt(t,e){var i=t.getBoundingClientRect();return{width:i.width/1,height:i.height/1,top:i.top/1,right:i.right/1,bottom:i.bottom/1,left:i.left/1,x:i.left/1,y:i.top/1}}function Kt(t){var e=Vt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Xt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Yt(t){return Wt(t).getComputedStyle(t)}function Qt(t){return["table","td","th"].indexOf(Rt(t))>=0}function Gt(t){return(($t(t)?t.ownerDocument:t.document)||window.document).documentElement}function Zt(t){return"html"===Rt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||Gt(t)}function Jt(t){return zt(t)&&"fixed"!==Yt(t).position?t.offsetParent:null}function te(t){for(var e=Wt(t),i=Jt(t);i&&Qt(i)&&"static"===Yt(i).position;)i=Jt(i);return i&&("html"===Rt(i)||"body"===Rt(i)&&"static"===Yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&zt(t)&&"fixed"===Yt(t).position)return null;for(var i=Zt(t);zt(i)&&["html","body"].indexOf(Rt(i))<0;){var n=Yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function ee(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var ie=Math.max,ne=Math.min,se=Math.round;function oe(t,e,i){return ie(t,ne(e,i))}function re(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ae(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const le={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Ut(i.placement),l=ee(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return re("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ae(t,yt))}(s.padding,i),d=Kt(o),u="y"===l?mt:bt,f="y"===l?gt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=te(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=oe(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Xt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ce(t){return t.split("-")[1]}var he={top:"auto",right:"auto",bottom:"auto",left:"auto"};function de(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:se(se(e*n)/n)||0,y:se(se(i*n)/n)||0}}(r):"function"==typeof h?h(r):r,u=d.x,f=void 0===u?0:u,p=d.y,m=void 0===p?0:p,g=r.hasOwnProperty("x"),_=r.hasOwnProperty("y"),b=bt,v=mt,y=window;if(c){var w=te(i),E="clientHeight",A="clientWidth";w===Wt(i)&&"static"!==Yt(w=Gt(i)).position&&"absolute"===a&&(E="scrollHeight",A="scrollWidth"),w=w,s!==mt&&(s!==bt&&s!==_t||o!==Et)||(v=gt,m-=w[E]-n.height,m*=l?1:-1),s!==bt&&(s!==mt&&s!==gt||o!==Et)||(b=_t,f-=w[A]-n.width,f*=l?1:-1)}var T,O=Object.assign({position:a},c&&he);return l?Object.assign({},O,((T={})[v]=_?"0":"",T[b]=g?"0":"",T.transform=(y.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",T)):Object.assign({},O,((e={})[v]=_?m+"px":"",e[b]=g?f+"px":"",e.transform="",e))}const ue={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Ut(e.placement),variation:ce(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,de(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,de(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var fe={passive:!0};const pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Wt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,fe)})),a&&l.addEventListener("resize",i.update,fe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,fe)})),a&&l.removeEventListener("resize",i.update,fe)}},data:{}};var me={left:"right",right:"left",bottom:"top",top:"bottom"};function ge(t){return t.replace(/left|right|bottom|top/g,(function(t){return me[t]}))}var _e={start:"end",end:"start"};function be(t){return t.replace(/start|end/g,(function(t){return _e[t]}))}function ve(t){var e=Wt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ye(t){return Vt(Gt(t)).left+ve(t).scrollLeft}function we(t){var e=Yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Rt(t))>=0?t.ownerDocument.body:zt(t)&&we(t)?t:Ee(Zt(t))}function Ae(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Wt(n),r=s?[o].concat(o.visualViewport||[],we(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ae(Zt(r)))}function Te(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e){return e===Tt?Te(function(t){var e=Wt(t),i=Gt(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ye(t),y:a}}(t)):zt(e)?function(t){var e=Vt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Te(function(t){var e,i=Gt(t),n=ve(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ie(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ie(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ye(t),l=-n.scrollTop;return"rtl"===Yt(s||i).direction&&(a+=ie(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Gt(t)))}function Ce(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Ut(s):null,r=s?ce(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case mt:e={x:a,y:i.y-n.height};break;case gt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?ee(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Et:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?At:o,a=i.rootBoundary,l=void 0===a?Tt:a,c=i.elementContext,h=void 0===c?Ot:c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=re("number"!=typeof p?p:ae(p,yt)),g=h===Ot?Ct:Ot,_=t.rects.popper,b=t.elements[u?g:h],v=function(t,e,i){var n="clippingParents"===e?function(t){var e=Ae(Zt(t)),i=["absolute","fixed"].indexOf(Yt(t).position)>=0&&zt(t)?te(t):t;return $t(i)?e.filter((function(t){return $t(t)&&Xt(t,i)&&"body"!==Rt(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Oe(t,i);return e.top=ie(n.top,e.top),e.right=ne(n.right,e.right),e.bottom=ne(n.bottom,e.bottom),e.left=ie(n.left,e.left),e}),Oe(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}($t(b)?b:b.contextElement||Gt(t.elements.popper),r,l),y=Vt(t.elements.reference),w=Ce({reference:y,element:_,strategy:"absolute",placement:s}),E=Te(Object.assign({},_,w)),A=h===Ot?E:y,T={top:v.top-A.top+m.top,bottom:A.bottom-v.bottom+m.bottom,left:v.left-A.left+m.left,right:A.right-v.right+m.right},O=t.modifiersData.offset;if(h===Ot&&O){var C=O[s];Object.keys(T).forEach((function(t){var e=[_t,gt].indexOf(t)>=0?1:-1,i=[mt,gt].indexOf(t)>=0?"y":"x";T[t]+=C[i]*e}))}return T}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?Lt:l,h=ce(n),d=h?a?kt:kt.filter((function(t){return ce(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Ut(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const xe={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=Ut(g),b=l||(_!==g&&p?function(t){if(Ut(t)===vt)return[];var e=ge(t);return[be(t),e,be(e)]}(g):[ge(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(Ut(i)===vt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=ke(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),N=x?L?_t:bt:L?gt:mt;y[D]>w[D]&&(N=ge(N));var I=ge(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[I]<=0),P.every((function(t){return t}))){T=C,A=!1;break}E.set(C,P)}if(A)for(var j=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Se(t){return[mt,_t,gt,bt].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Se(l),d=Se(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Ie={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=Lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Ut(t),s=[bt,mt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Pe={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Ce({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},je={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Ut(e.placement),b=ce(e.placement),v=!b,y=ee(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?mt:bt,L="y"===y?gt:_t,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],N=E[y]-g[L],I=f?-T[x]/2:0,P=b===wt?A[x]:T[x],j=b===wt?-T[x]:-A[x],M=e.elements.arrow,H=f&&M?Kt(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},R=B[k],W=B[L],$=oe(0,A[x],H[x]),z=v?A[x]/2-I-$-R-O:P-$-R-O,q=v?-A[x]/2+I+$+W+O:j+$+W+O,F=e.elements.arrow&&te(e.elements.arrow),U=F?"y"===y?F.clientTop||0:F.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-U,X=E[y]+q-V;if(o){var Y=oe(f?ne(S,K):S,D,f?ie(N,X):N);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?mt:bt,G="x"===y?gt:_t,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=oe(f?ne(J,K):J,Z,f?ie(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function Me(t,e,i){void 0===i&&(i=!1);var n=zt(e);zt(e)&&function(t){var e=t.getBoundingClientRect();e.width,t.offsetWidth,e.height,t.offsetHeight}(e);var s,o,r=Gt(e),a=Vt(t),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(n||!n&&!i)&&(("body"!==Rt(e)||we(r))&&(l=(s=e)!==Wt(s)&&zt(s)?{scrollLeft:(o=s).scrollLeft,scrollTop:o.scrollTop}:ve(s)),zt(e)?((c=Vt(e)).x+=e.clientLeft,c.y+=e.clientTop):r&&(c.x=ye(r))),{x:a.left+l.scrollLeft-c.x,y:a.top+l.scrollTop-c.y,width:a.width,height:a.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Re(){for(var t=arguments.length,e=new Array(t),i=0;ij.on(t,"mouseover",d))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Je),this._element.classList.add(Je),j.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(c(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){j.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._popper&&this._popper.destroy(),this._menu.classList.remove(Je),this._element.classList.remove(Je),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},a(Ue,t,this.constructor.DefaultType),"object"==typeof t.reference&&!o(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ue.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(t){if(void 0===Fe)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:o(this._config.reference)?e=r(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find((t=>"applyStyles"===t.name&&!1===t.enabled));this._popper=qe(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains(Je)}_getMenuElement(){return V.next(this._element,ei)[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ri;if(t.classList.contains("dropstart"))return ai;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ni:ii:e?oi:si}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(l);i.length&&v(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=V.find(ti);for(let i=0,n=e.length;ie+t)),this._setElementAttributes(di,"paddingRight",(e=>e+t)),this._setElementAttributes(ui,"marginRight",(e=>e-t))}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=`${i(Number.parseFloat(s))}px`}))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(di,"paddingRight"),this._resetElementAttributes(ui,"marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)}))}_applyManipulationCallback(t,e){o(t)?e(t):V.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const pi={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mi={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"},gi="show",_i="mousedown.bs.backdrop";class bi{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&u(this._getElement()),this._getElement().classList.add(gi),this._emulateAnimation((()=>{_(t)}))):_(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove(gi),this._emulateAnimation((()=>{this.dispose(),_(t)}))):_(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...pi,..."object"==typeof t?t:{}}).rootElement=r(t.rootElement),a("backdrop",t,mi),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),j.on(this._getElement(),_i,(()=>{_(this._config.clickCallback)})),this._isAppended=!0)}dispose(){this._isAppended&&(j.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const vi={trapElement:null,autofocus:!0},yi={trapElement:"element",autofocus:"boolean"},wi=".bs.focustrap",Ei="backward";class Ai{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),j.off(document,wi),j.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),j.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,wi))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const n=V.focusableChildren(i);0===n.length?i.focus():this._lastTabNavDirection===Ei?n[n.length-1].focus():n[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ei:"forward")}_getConfig(t){return t={...vi,..."object"==typeof t?t:{}},a("focustrap",t,yi),t}}const Ti="modal",Oi="Escape",Ci={backdrop:!0,keyboard:!0,focus:!0},ki={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},Li="hidden.bs.modal",xi="show.bs.modal",Di="resize.bs.modal",Si="click.dismiss.bs.modal",Ni="keydown.dismiss.bs.modal",Ii="mousedown.dismiss.bs.modal",Pi="modal-open",ji="show",Mi="modal-static";class Hi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new fi}static get Default(){return Ci}static get NAME(){return Ti}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add(Pi),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),j.on(this._dialog,Ii,(()=>{j.one(this._element,"mouseup.dismiss.bs.modal",(t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)}))})),this._showBackdrop((()=>this._showElement(t))))}hide(){if(!this._isShown||this._isTransitioning)return;if(j.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove(ji),j.off(this._element,Si),j.off(this._dialog,Ii),this._queueCallback((()=>this._hideModal()),this._element,t)}dispose(){[window,this._dialog].forEach((t=>j.off(t,".bs.modal"))),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_getConfig(t){return t={...Ci,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Ti,t,ki),t}_showElement(t){const e=this._isAnimated(),i=V.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add(ji),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,e)}_setEscapeEvent(){this._isShown?j.on(this._element,Ni,(t=>{this._config.keyboard&&t.key===Oi?(t.preventDefault(),this.hide()):this._config.keyboard||t.key!==Oi||this._triggerBackdropTransition()})):j.off(this._element,Ni)}_setResizeEvent(){this._isShown?j.on(window,Di,(()=>this._adjustDialog())):j.off(window,Di)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Pi),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,Li)}))}_showBackdrop(t){j.on(this._element,Si,(t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())})),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains(Mi)||(n||(i.overflowY="hidden"),t.add(Mi),this._queueCallback((()=>{t.remove(Mi),n||this._queueCallback((()=>{i.overflowY=""}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!m()||i&&!t&&m())&&(this._element.style.paddingLeft=`${e}px`),(i&&!t&&!m()||!i&&t&&m())&&(this._element.style.paddingRight=`${e}px`)}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,xi,(t=>{t.defaultPrevented||j.one(e,Li,(()=>{l(this)&&this.focus()}))}));const i=V.findOne(".modal.show");i&&Hi.getInstance(i).hide(),Hi.getOrCreateInstance(e).toggle(this)})),R(Hi),g(Hi);const Bi="offcanvas",Ri={backdrop:!0,keyboard:!0,scroll:!1},Wi={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},$i="show",zi=".offcanvas.show",qi="hidden.bs.offcanvas";class Fi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return Bi}static get Default(){return Ri}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new fi).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($i),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),j.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove($i),this._backdrop.hide(),this._queueCallback((()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new fi).reset(),j.trigger(this._element,qi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...Ri,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Bi,t,Wi),t}_initializeBackDrop(){return new bi({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_addEventListeners(){j.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}))}static jQueryInterface(t){return this.each((function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;j.one(e,qi,(()=>{l(this)&&this.focus()}));const i=V.findOne(zi);i&&i!==e&&Fi.getInstance(i).hide(),Fi.getOrCreateInstance(e).toggle(this)})),j.on(window,"load.bs.offcanvas.data-api",(()=>V.find(zi).forEach((t=>Fi.getOrCreateInstance(t).show())))),R(Fi),g(Fi);const Ui=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Vi=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Ki=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Xi=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!Ui.has(i)||Boolean(Vi.test(t.nodeValue)||Ki.test(t.nodeValue));const n=e.filter((t=>t instanceof RegExp));for(let t=0,e=n.length;t{Xi(t,r)||i.removeAttribute(t.nodeName)}))}return n.body.innerHTML}const Qi="tooltip",Gi=new Set(["sanitize","allowList","sanitizeFn"]),Zi={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ji={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},tn={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},en={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},nn="fade",sn="show",on="show",rn="out",an=".tooltip-inner",ln=".modal",cn="hide.bs.modal",hn="hover",dn="focus";class un extends B{constructor(t,e){if(void 0===Fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return tn}static get NAME(){return Qi}static get Event(){return en}static get DefaultType(){return Zi}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains(sn))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(ln),cn,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.Event.SHOW),e=h(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;"tooltip"===this.constructor.NAME&&this.tip&&this.getTitle()!==this.tip.querySelector(an).innerHTML&&(this._disposePopper(),this.tip.remove(),this.tip=null);const n=this.getTipElement(),s=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",s),this._element.setAttribute("aria-describedby",s),this._config.animation&&n.classList.add(nn);const o="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,r=this._getAttachment(o);this._addAttachmentClass(r);const{container:a}=this._config;H.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(a.append(n),j.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=qe(this._element,n,this._getPopperConfig(r)),n.classList.add(sn);const l=this._resolvePossibleFunction(this._config.customClass);l&&n.classList.add(...l.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>{j.on(t,"mouseover",d)}));const c=this.tip.classList.contains(nn);this._queueCallback((()=>{const t=this._hoverState;this._hoverState=null,j.trigger(this._element,this.constructor.Event.SHOWN),t===rn&&this._leave(null,this)}),this.tip,c)}hide(){if(!this._popper)return;const t=this.getTipElement();if(j.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove(sn),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains(nn);this._queueCallback((()=>{this._isWithActiveTrigger()||(this._hoverState!==on&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.Event.HIDDEN),this._disposePopper())}),this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove(nn,sn),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),an)}_sanitizeAndSetContent(t,e,i){const n=V.findOne(i,t);e||!n?this.setElementContent(n,e):n.remove()}setElementContent(t,e){if(null!==t)return o(e)?(e=r(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Yi(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return Ji[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach((t=>{if("click"===t)j.on(this._element,this.constructor.Event.CLICK,this._config.selector,(t=>this.toggle(t)));else if("manual"!==t){const e=t===hn?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i=t===hn?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;j.on(this._element,e,this._config.selector,(t=>this._enter(t))),j.on(this._element,i,this._config.selector,(t=>this._leave(t)))}})),this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(ln),cn,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?dn:hn]=!0),e.getTipElement().classList.contains(sn)||e._hoverState===on?e._hoverState=on:(clearTimeout(e._timeout),e._hoverState=on,e._config.delay&&e._config.delay.show?e._timeout=setTimeout((()=>{e._hoverState===on&&e.show()}),e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?dn:hn]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=rn,e._config.delay&&e._config.delay.hide?e._timeout=setTimeout((()=>{e._hoverState===rn&&e.hide()}),e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach((t=>{Gi.has(t)&&delete e[t]})),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),a(Qi,t,this.constructor.DefaultType),t.sanitize&&(t.template=Yi(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map((t=>t.trim())).forEach((e=>t.classList.remove(e)))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn={...un.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},pn={...un.DefaultType,content:"(string|element|function)"},mn={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class gn extends un{static get Default(){return fn}static get NAME(){return"popover"}static get Event(){return mn}static get DefaultType(){return pn}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=gn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(gn);const _n="scrollspy",bn={offset:10,method:"auto",target:""},vn={offset:"number",method:"string",target:"(string|element)"},yn="active",wn=".nav-link, .list-group-item, .dropdown-item",En="position";class An extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,j.on(this._scrollElement,"scroll.bs.scrollspy",(()=>this._process())),this.refresh(),this._process()}static get Default(){return bn}static get NAME(){return _n}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":En,e="auto"===this._config.method?t:this._config.method,n=e===En?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(wn,this._config.target).map((t=>{const s=i(t),o=s?V.findOne(s):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[U[e](o).top+n,s]}return null})).filter((t=>t)).sort(((t,e)=>t[0]-e[0])).forEach((t=>{this._offsets.push(t[0]),this._targets.push(t[1])}))}dispose(){j.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...bn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=r(t.target)||document.documentElement,a(_n,t,vn),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`)),i=V.findOne(e.join(","),this._config.target);i.classList.add(yn),i.classList.contains("dropdown-item")?V.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add(yn):V.parents(i,".nav, .list-group").forEach((t=>{V.prev(t,".nav-link, .list-group-item").forEach((t=>t.classList.add(yn))),V.prev(t,".nav-item").forEach((t=>{V.children(t,".nav-link").forEach((t=>t.classList.add(yn)))}))})),j.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){V.find(wn,this._config.target).filter((t=>t.classList.contains(yn))).forEach((t=>t.classList.remove(yn)))}static jQueryInterface(t){return this.each((function(){const e=An.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,"load.bs.scrollspy.data-api",(()=>{V.find('[data-bs-spy="scroll"]').forEach((t=>new An(t)))})),g(An);const Tn="active",On="fade",Cn="show",kn=".active",Ln=":scope > li > .active";class xn extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Tn))return;let t;const e=n(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?Ln:kn;t=V.find(e,i),t=t[t.length-1]}const s=t?j.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(j.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const o=()=>{j.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),j.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,kn):V.find(Ln,e))[0],s=i&&n&&n.classList.contains(On),o=()=>this._transitionComplete(t,n,i);n&&s?(n.classList.remove(Cn),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove(Tn);const t=V.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove(Tn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add(Tn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains(On)&&t.classList.add(Cn);let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&V.find(".dropdown-toggle",e).forEach((t=>t.classList.add(Tn))),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||xn.getOrCreateInstance(this).show()})),g(xn);const Dn="toast",Sn="hide",Nn="show",In="showing",Pn={animation:"boolean",autohide:"boolean",delay:"number"},jn={animation:!0,autohide:!0,delay:5e3};class Mn extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Pn}static get Default(){return jn}static get NAME(){return Dn}show(){j.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Sn),u(this._element),this._element.classList.add(Nn),this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.remove(In),j.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this._element.classList.contains(Nn)&&(j.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.add(Sn),this._element.classList.remove(In),this._element.classList.remove(Nn),j.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains(Nn)&&this._element.classList.remove(Nn),super.dispose()}_getConfig(t){return t={...jn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},a(Dn,t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),j.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Mn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Mn),g(Mn),{Alert:W,Button:z,Carousel:st,Collapse:pt,Dropdown:hi,Modal:Hi,Offcanvas:Fi,Popover:gn,ScrollSpy:An,Tab:xn,Toast:Mn,Tooltip:un}})); +!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[r]||{},h=e.startsWith(".");if(void 0===o){if(h)for(const i of Object.keys(l))I(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(A,"");a&&!e.includes(s)||$(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;$(t,l,r,o,s?i:null)}},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f();let s=null,o=!0,r=!0,a=!1;e!==N(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=j(new Event(e,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function j(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function M(t){if("true"===t)return!0;if("false"===t)return!1;if(t===Number(t).toString())return Number(t);if(""===t||"null"===t)return null;if("string"!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function F(t){return t.replace(/[A-Z]/g,t=>`-${t.toLowerCase()}`)}const H={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter(t=>t.startsWith("bs")&&!t.startsWith("bsConfig"));for(const n of i){let i=n.replace(/^bs/,"");i=i.charAt(0).toLowerCase()+i.slice(1),e[i]=M(t.dataset[n])}return e},getDataAttribute:(t,e)=>M(t.getAttribute(`data-bs-${F(e)}`))};class W{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=r(e)?H.getDataAttribute(e,"config"):{};return{...this.constructor.Default,..."object"==typeof i?i:{},...r(e)?H.getDataAttributes(e):{},..."object"==typeof t?t:{}}}_typeCheckConfig(t,e=this.constructor.DefaultType){for(const[i,n]of Object.entries(e)){const e=t[i],o=r(e)?"element":s(e);if(!new RegExp(n).test(o))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option "${i}" provided type "${o}" but expected type "${n}".`)}}}class B extends W{constructor(t,i){super(),(t=a(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element,this.constructor.DATA_KEY),P.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){b(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(a(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.3.8"}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const z=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?e.split(",").map(t=>n(t)).join(","):null},R={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter(t=>t.matches(e)),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map(t=>`${t}:not([tabindex^="-"])`).join(",");return this.find(e,t).filter(t=>!c(t)&&l(t))},getSelectorFromElement(t){const e=z(t);return e&&R.findOne(e)?e:null},getElementFromSelector(t){const e=z(t);return e?R.findOne(e):null},getMultipleElementsFromSelector(t){const e=z(t);return e?R.find(e):[]}},q=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;P.on(document,i,`[data-bs-dismiss="${n}"]`,function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const s=R.getElementFromSelector(this)||this.closest(`.${n}`);t.getOrCreateInstance(s)[e]()})},V=".bs.alert",K=`close${V}`,Q=`closed${V}`;class X extends B{static get NAME(){return"alert"}close(){if(P.trigger(this._element,K).defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback(()=>this._destroyElement(),this._element,t)}_destroyElement(){this._element.remove(),P.trigger(this._element,Q),this.dispose()}static jQueryInterface(t){return this.each(function(){const e=X.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}q(X,"close"),g(X);const Y='[data-bs-toggle="button"]';class U extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each(function(){const e=U.getOrCreateInstance(this);"toggle"===t&&e[t]()})}}P.on(document,"click.bs.button.data-api",Y,t=>{t.preventDefault();const e=t.target.closest(Y);U.getOrCreateInstance(e).toggle()}),g(U);const G=".bs.swipe",J=`touchstart${G}`,Z=`touchmove${G}`,tt=`touchend${G}`,et=`pointerdown${G}`,it=`pointerup${G}`,nt={endCallback:null,leftCallback:null,rightCallback:null},st={endCallback:"(function|null)",leftCallback:"(function|null)",rightCallback:"(function|null)"};class ot extends W{constructor(t,e){super(),this._element=t,t&&ot.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent),this._initEvents())}static get Default(){return nt}static get DefaultType(){return st}static get NAME(){return"swipe"}dispose(){P.off(this._element,G)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),_(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&_(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(P.on(this._element,et,t=>this._start(t)),P.on(this._element,it,t=>this._end(t)),this._element.classList.add("pointer-event")):(P.on(this._element,J,t=>this._start(t)),P.on(this._element,Z,t=>this._move(t)),P.on(this._element,tt,t=>this._end(t)))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&("pen"===t.pointerType||"touch"===t.pointerType)}static isSupported(){return"ontouchstart"in document.documentElement||navigator.maxTouchPoints>0}}const rt=".bs.carousel",at=".data-api",lt="ArrowLeft",ct="ArrowRight",ht="next",dt="prev",ut="left",ft="right",pt=`slide${rt}`,mt=`slid${rt}`,gt=`keydown${rt}`,_t=`mouseenter${rt}`,bt=`mouseleave${rt}`,vt=`dragstart${rt}`,yt=`load${rt}${at}`,wt=`click${rt}${at}`,At="carousel",Et="active",Tt=".active",Ct=".carousel-item",Ot=Tt+Ct,xt={[lt]:ft,[ct]:ut},kt={interval:5e3,keyboard:!0,pause:"hover",ride:!1,touch:!0,wrap:!0},Lt={interval:"(number|boolean)",keyboard:"boolean",pause:"(string|boolean)",ride:"(boolean|string)",touch:"boolean",wrap:"boolean"};class St extends B{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=R.findOne(".carousel-indicators",this._element),this._addEventListeners(),this._config.ride===At&&this.cycle()}static get Default(){return kt}static get DefaultType(){return Lt}static get NAME(){return"carousel"}next(){this._slide(ht)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(dt)}pause(){this._isSliding&&o(this._element),this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval(()=>this.nextWhenVisible(),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?P.one(this._element,mt,()=>this.cycle()):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void P.one(this._element,mt,()=>this.to(t));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?ht:dt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose(),super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&P.on(this._element,gt,t=>this._keydown(t)),"hover"===this._config.pause&&(P.on(this._element,_t,()=>this.pause()),P.on(this._element,bt,()=>this._maybeEnableCycle())),this._config.touch&&ot.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of R.find(".carousel-item img",this._element))P.on(t,vt,t=>t.preventDefault());const t={leftCallback:()=>this._slide(this._directionToOrder(ut)),rightCallback:()=>this._slide(this._directionToOrder(ft)),endCallback:()=>{"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(()=>this._maybeEnableCycle(),500+this._config.interval))}};this._swipeHelper=new ot(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=xt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return;const e=R.findOne(Tt,this._indicatorsElement);e.classList.remove(Et),e.removeAttribute("aria-current");const i=R.findOne(`[data-bs-slide-to="${t}"]`,this._indicatorsElement);i&&(i.classList.add(Et),i.setAttribute("aria-current","true"))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute("data-bs-interval"),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive(),n=t===ht,s=e||v(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>P.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(pt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?"carousel-item-start":"carousel-item-end",c=n?"carousel-item-next":"carousel-item-prev";s.classList.add(c),u(s),i.classList.add(l),s.classList.add(l),this._queueCallback(()=>{s.classList.remove(l,c),s.classList.add(Et),i.classList.remove(Et,c,l),this._isSliding=!1,r(mt)},i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains("slide")}_getActive(){return R.findOne(Ot,this._element)}_getItems(){return R.find(Ct,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return m()?t===ut?dt:ht:t===ut?ht:dt}_orderToDirection(t){return m()?t===dt?ut:ft:t===dt?ft:ut}static jQueryInterface(t){return this.each(function(){const e=St.getOrCreateInstance(this,t);if("number"!=typeof t){if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}}else e.to(t)})}}P.on(document,wt,"[data-bs-slide], [data-bs-slide-to]",function(t){const e=R.getElementFromSelector(this);if(!e||!e.classList.contains(At))return;t.preventDefault();const i=St.getOrCreateInstance(e),n=this.getAttribute("data-bs-slide-to");return n?(i.to(n),void i._maybeEnableCycle()):"next"===H.getDataAttribute(this,"slide")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())}),P.on(window,yt,()=>{const t=R.find('[data-bs-ride="carousel"]');for(const e of t)St.getOrCreateInstance(e)}),g(St);const Dt=".bs.collapse",$t=`show${Dt}`,It=`shown${Dt}`,Nt=`hide${Dt}`,Pt=`hidden${Dt}`,jt=`click${Dt}.data-api`,Mt="show",Ft="collapse",Ht="collapsing",Wt=`:scope .${Ft} .${Ft}`,Bt='[data-bs-toggle="collapse"]',zt={parent:null,toggle:!0},Rt={parent:"(null|element)",toggle:"boolean"};class qt extends B{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=R.find(Bt);for(const t of i){const e=R.getSelectorFromElement(t),i=R.find(e).filter(t=>t===this._element);null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return zt}static get DefaultType(){return Rt}static get NAME(){return"collapse"}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren(".collapse.show, .collapse.collapsing").filter(t=>t!==this._element).map(t=>qt.getOrCreateInstance(t,{toggle:!1}))),t.length&&t[0]._isTransitioning)return;if(P.trigger(this._element,$t).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension();this._element.classList.remove(Ft),this._element.classList.add(Ht),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(Ht),this._element.classList.add(Ft,Mt),this._element.style[e]="",P.trigger(this._element,It)},this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(P.trigger(this._element,Nt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(Ht),this._element.classList.remove(Ft,Mt);for(const t of this._triggerArray){const e=R.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="",this._queueCallback(()=>{this._isTransitioning=!1,this._element.classList.remove(Ht),this._element.classList.add(Ft),P.trigger(this._element,Pt)},this._element,!0)}_isShown(t=this._element){return t.classList.contains(Mt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=a(t.parent),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Bt);for(const e of t){const t=R.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const e=R.find(Wt,this._config.parent);return R.find(t,this._config.parent).filter(t=>!e.includes(t))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle("collapsed",!e),i.setAttribute("aria-expanded",e)}static jQueryInterface(t){const e={};return"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each(function(){const i=qt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}})}}P.on(document,jt,Bt,function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();for(const t of R.getMultipleElementsFromSelector(this))qt.getOrCreateInstance(t,{toggle:!1}).toggle()}),g(qt);var Vt="top",Kt="bottom",Qt="right",Xt="left",Yt="auto",Ut=[Vt,Kt,Qt,Xt],Gt="start",Jt="end",Zt="clippingParents",te="viewport",ee="popper",ie="reference",ne=Ut.reduce(function(t,e){return t.concat([e+"-"+Gt,e+"-"+Jt])},[]),se=[].concat(Ut,[Yt]).reduce(function(t,e){return t.concat([e,e+"-"+Gt,e+"-"+Jt])},[]),oe="beforeRead",re="read",ae="afterRead",le="beforeMain",ce="main",he="afterMain",de="beforeWrite",ue="write",fe="afterWrite",pe=[oe,re,ae,le,ce,he,de,ue,fe];function me(t){return t?(t.nodeName||"").toLowerCase():null}function ge(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function _e(t){return t instanceof ge(t).Element||t instanceof Element}function be(t){return t instanceof ge(t).HTMLElement||t instanceof HTMLElement}function ve(t){return"undefined"!=typeof ShadowRoot&&(t instanceof ge(t).ShadowRoot||t instanceof ShadowRoot)}const ye={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach(function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];be(s)&&me(s)&&(Object.assign(s.style,i),Object.keys(n).forEach(function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)}))})},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach(function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce(function(t,e){return t[e]="",t},{});be(n)&&me(n)&&(Object.assign(n.style,o),Object.keys(s).forEach(function(t){n.removeAttribute(t)}))})}},requires:["computeStyles"]};function we(t){return t.split("-")[0]}var Ae=Math.max,Ee=Math.min,Te=Math.round;function Ce(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map(function(t){return t.brand+"/"+t.version}).join(" "):navigator.userAgent}function Oe(){return!/^((?!chrome|android).)*safari/i.test(Ce())}function xe(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&be(t)&&(s=t.offsetWidth>0&&Te(n.width)/t.offsetWidth||1,o=t.offsetHeight>0&&Te(n.height)/t.offsetHeight||1);var r=(_e(t)?ge(t):window).visualViewport,a=!Oe()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function ke(t){var e=xe(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Le(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&ve(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Se(t){return ge(t).getComputedStyle(t)}function De(t){return["table","td","th"].indexOf(me(t))>=0}function $e(t){return((_e(t)?t.ownerDocument:t.document)||window.document).documentElement}function Ie(t){return"html"===me(t)?t:t.assignedSlot||t.parentNode||(ve(t)?t.host:null)||$e(t)}function Ne(t){return be(t)&&"fixed"!==Se(t).position?t.offsetParent:null}function Pe(t){for(var e=ge(t),i=Ne(t);i&&De(i)&&"static"===Se(i).position;)i=Ne(i);return i&&("html"===me(i)||"body"===me(i)&&"static"===Se(i).position)?e:i||function(t){var e=/firefox/i.test(Ce());if(/Trident/i.test(Ce())&&be(t)&&"fixed"===Se(t).position)return null;var i=Ie(t);for(ve(i)&&(i=i.host);be(i)&&["html","body"].indexOf(me(i))<0;){var n=Se(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function je(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}function Me(t,e,i){return Ae(t,Ee(e,i))}function Fe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function He(t,e){return e.reduce(function(e,i){return e[i]=t,e},{})}const We={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=we(i.placement),l=je(a),c=[Xt,Qt].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return Fe("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:He(t,Ut))}(s.padding,i),d=ke(o),u="y"===l?Vt:Xt,f="y"===l?Kt:Qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=Pe(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Me(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Le(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function Be(t){return t.split("-")[1]}var ze={top:"auto",right:"auto",bottom:"auto",left:"auto"};function Re(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u,p=r.y,m=void 0===p?0:p,g="function"==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty("x"),b=r.hasOwnProperty("y"),v=Xt,y=Vt,w=window;if(c){var A=Pe(i),E="clientHeight",T="clientWidth";A===ge(i)&&"static"!==Se(A=$e(i)).position&&"absolute"===a&&(E="scrollHeight",T="scrollWidth"),(s===Vt||(s===Xt||s===Qt)&&o===Jt)&&(y=Kt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Xt&&(s!==Vt&&s!==Kt||o!==Jt)||(v=Qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&ze),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:Te(i*s)/s||0,y:Te(n*s)/s||0}}({x:f,y:m},ge(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?"0":"",C[v]=_?"0":"",C.transform=(w.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",C)):Object.assign({},O,((e={})[y]=b?m+"px":"",e[v]=_?f+"px":"",e.transform="",e))}const qe={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:we(e.placement),variation:Be(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:"fixed"===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,Re(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,Re(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var Ve={passive:!0};const Ke={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=ge(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach(function(t){t.addEventListener("scroll",i.update,Ve)}),a&&l.addEventListener("resize",i.update,Ve),function(){o&&c.forEach(function(t){t.removeEventListener("scroll",i.update,Ve)}),a&&l.removeEventListener("resize",i.update,Ve)}},data:{}};var Qe={left:"right",right:"left",bottom:"top",top:"bottom"};function Xe(t){return t.replace(/left|right|bottom|top/g,function(t){return Qe[t]})}var Ye={start:"end",end:"start"};function Ue(t){return t.replace(/start|end/g,function(t){return Ye[t]})}function Ge(t){var e=ge(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Je(t){return xe($e(t)).left+Ge(t).scrollLeft}function Ze(t){var e=Se(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function ti(t){return["html","body","#document"].indexOf(me(t))>=0?t.ownerDocument.body:be(t)&&Ze(t)?t:ti(Ie(t))}function ei(t,e){var i;void 0===e&&(e=[]);var n=ti(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=ge(n),r=s?[o].concat(o.visualViewport||[],Ze(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(ei(Ie(r)))}function ii(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ni(t,e,i){return e===te?ii(function(t,e){var i=ge(t),n=$e(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Oe();(c||!c&&"fixed"===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Je(t),y:l}}(t,i)):_e(e)?function(t,e){var i=xe(t,!1,"fixed"===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):ii(function(t){var e,i=$e(t),n=Ge(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=Ae(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=Ae(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Je(t),l=-n.scrollTop;return"rtl"===Se(s||i).direction&&(a+=Ae(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}($e(t)))}function si(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?we(s):null,r=s?Be(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case Vt:e={x:a,y:i.y-n.height};break;case Kt:e={x:a,y:i.y+i.height};break;case Qt:e={x:i.x+i.width,y:l};break;case Xt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?je(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case Gt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Jt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function oi(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Zt:a,c=i.rootBoundary,h=void 0===c?te:c,d=i.elementContext,u=void 0===d?ee:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Fe("number"!=typeof g?g:He(g,Ut)),b=u===ee?ie:ee,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s="clippingParents"===e?function(t){var e=ei(Ie(t)),i=["absolute","fixed"].indexOf(Se(t).position)>=0&&be(t)?Pe(t):t;return _e(i)?e.filter(function(t){return _e(t)&&Le(t,i)&&"body"!==me(t)}):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce(function(e,i){var s=ni(t,i,n);return e.top=Ae(s.top,e.top),e.right=Ee(s.right,e.right),e.bottom=Ee(s.bottom,e.bottom),e.left=Ae(s.left,e.left),e},ni(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(_e(y)?y:y.contextElement||$e(t.elements.popper),l,h,r),A=xe(t.elements.reference),E=si({reference:A,element:v,placement:s}),T=ii(Object.assign({},v,E)),C=u===ee?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===ee&&x){var k=x[s];Object.keys(O).forEach(function(t){var e=[Qt,Kt].indexOf(t)>=0?1:-1,i=[Vt,Kt].indexOf(t)>=0?"y":"x";O[t]+=k[i]*e})}return O}function ri(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?se:l,h=Be(n),d=h?a?ne:ne.filter(function(t){return Be(t)===h}):Ut,u=d.filter(function(t){return c.indexOf(t)>=0});0===u.length&&(u=d);var f=u.reduce(function(e,i){return e[i]=oi(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[we(i)],e},{});return Object.keys(f).sort(function(t,e){return f[t]-f[e]})}const ai={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=we(g),b=l||(_!==g&&p?function(t){if(we(t)===Yt)return[];var e=Xe(t);return[Ue(t),e,Ue(e)]}(g):[Xe(g)]),v=[g].concat(b).reduce(function(t,i){return t.concat(we(i)===Yt?ri(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)},[]),y=e.rects.reference,w=e.rects.popper,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?"width":"height",D=oi(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?Qt:Xt:k?Kt:Vt;y[S]>w[S]&&($=Xe($));var I=Xe($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every(function(t){return t})){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find(function(e){var i=A.get(e);if(i)return i.slice(0,t).every(function(t){return t})});if(e)return T=e,"break"},j=p?3:1;j>0&&"break"!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function li(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ci(t){return[Vt,Qt,Kt,Xt].some(function(e){return t[e]>=0})}const hi={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=oi(e,{elementContext:"reference"}),a=oi(e,{altBoundary:!0}),l=li(r,n),c=li(a,s,o),h=ci(l),d=ci(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},di={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=se.reduce(function(t,i){return t[i]=function(t,e,i){var n=we(t),s=[Xt,Vt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Xt,Qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t},{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ui={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=si({reference:e.rects.reference,element:e.rects.popper,placement:e.placement})},data:{}},fi={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=oi(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=we(e.placement),b=Be(e.placement),v=!b,y=je(_),w="x"===y?"y":"x",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O="number"==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S="y"===y?Vt:Xt,D="y"===y?Kt:Qt,$="y"===y?"height":"width",I=A[y],N=I+g[S],P=I-g[D],j=f?-T[$]/2:0,M=b===Gt?E[$]:T[$],F=b===Gt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?ke(H):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Me(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&Pe(e.elements.arrow),X=Q?"y"===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Me(f?Ee(N,I+V-Y-X):N,I,f?Ae(P,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z="x"===y?Vt:Xt,tt="x"===y?Kt:Qt,et=A[w],it="y"===w?"height":"width",nt=et+g[Z],st=et-g[tt],ot=-1!==[Vt,Xt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Me(t,e,i);return n>i?i:n}(at,et,lt):Me(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:["offset"]};function pi(t,e,i){void 0===i&&(i=!1);var n,s,o=be(e),r=be(e)&&function(t){var e=t.getBoundingClientRect(),i=Te(e.width)/t.offsetWidth||1,n=Te(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=$e(e),l=xe(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&(("body"!==me(e)||Ze(a))&&(c=(n=e)!==ge(n)&&be(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Ge(n)),be(e)?((h=xe(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Je(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function mi(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach(function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}}),n.push(t)}return t.forEach(function(t){e.set(t.name,t)}),t.forEach(function(t){i.has(t.name)||s(t)}),n}var gi={placement:"bottom",modifiers:[],strategy:"absolute"};function _i(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return(this._inNavbar||"static"===this._config.display)&&(H.setDataAttribute(this._menu,"popper","static"),t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..._(this._config.popperConfig,[void 0,t])}}_selectMenuItem({key:t,target:e}){const i=R.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(t=>l(t));i.length&&v(i,e,t===xi,!i.includes(e)).focus()}static jQueryInterface(t){return this.each(function(){const e=Qi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}static clearMenus(t){if(2===t.button||"keyup"===t.type&&"Tab"!==t.key)return;const e=R.find(Mi);for(const i of e){const e=Qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||"inside"===e._config.autoClose&&!s||"outside"===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&("keyup"===t.type&&"Tab"===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};"click"===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const e=/input|textarea/i.test(t.target.tagName),i="Escape"===t.key,n=[Oi,xi].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(ji)?this:R.prev(this,ji)[0]||R.next(this,ji)[0]||R.findOne(ji,t.delegateTarget.parentNode),o=Qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}P.on(document,Ii,ji,Qi.dataApiKeydownHandler),P.on(document,Ii,Fi,Qi.dataApiKeydownHandler),P.on(document,$i,Qi.clearMenus),P.on(document,Ni,Qi.clearMenus),P.on(document,$i,ji,function(t){t.preventDefault(),Qi.getOrCreateInstance(this).toggle()}),g(Qi);const Xi="backdrop",Yi="show",Ui=`mousedown.bs.${Xi}`,Gi={className:"modal-backdrop",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:"body"},Ji={className:"string",clickCallback:"(function|null)",isAnimated:"boolean",isVisible:"boolean",rootElement:"(element|string)"};class Zi extends W{constructor(t){super(),this._config=this._getConfig(t),this._isAppended=!1,this._element=null}static get Default(){return Gi}static get DefaultType(){return Ji}static get NAME(){return Xi}show(t){if(!this._config.isVisible)return void _(t);this._append();const e=this._getElement();this._config.isAnimated&&u(e),e.classList.add(Yi),this._emulateAnimation(()=>{_(t)})}hide(t){this._config.isVisible?(this._getElement().classList.remove(Yi),this._emulateAnimation(()=>{this.dispose(),_(t)})):_(t)}dispose(){this._isAppended&&(P.off(this._element,Ui),this._element.remove(),this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=a(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),P.on(t,Ui,()=>{_(this._config.clickCallback)}),this._isAppended=!0}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const tn=".bs.focustrap",en=`focusin${tn}`,nn=`keydown.tab${tn}`,sn="backward",on={autofocus:!0,trapElement:null},rn={autofocus:"boolean",trapElement:"element"};class an extends W{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return on}static get DefaultType(){return rn}static get NAME(){return"focustrap"}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),P.off(document,tn),P.on(document,en,t=>this._handleFocusin(t)),P.on(document,nn,t=>this._handleKeydown(t)),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,P.off(document,tn))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=R.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===sn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?sn:"forward")}}const ln=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",cn=".sticky-top",hn="padding-right",dn="margin-right";class un{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,hn,e=>e+t),this._setElementAttributes(ln,hn,e=>e+t),this._setElementAttributes(cn,dn,e=>e-t)}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,hn),this._resetElementAttributes(ln,hn),this._resetElementAttributes(cn,dn)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)})}_saveInitialAttribute(t,e){const i=t.style.getPropertyValue(e);i&&H.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,t=>{const i=H.getDataAttribute(t,e);null!==i?(H.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)})}_applyManipulationCallback(t,e){if(r(t))e(t);else for(const i of R.find(t,this._element))e(i)}}const fn=".bs.modal",pn=`hide${fn}`,mn=`hidePrevented${fn}`,gn=`hidden${fn}`,_n=`show${fn}`,bn=`shown${fn}`,vn=`resize${fn}`,yn=`click.dismiss${fn}`,wn=`mousedown.dismiss${fn}`,An=`keydown.dismiss${fn}`,En=`click${fn}.data-api`,Tn="modal-open",Cn="show",On="modal-static",xn={backdrop:!0,focus:!0,keyboard:!0},kn={backdrop:"(boolean|string)",focus:"boolean",keyboard:"boolean"};class Ln extends B{constructor(t,e){super(t,e),this._dialog=R.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new un,this._addEventListeners()}static get Default(){return xn}static get DefaultType(){return kn}static get NAME(){return"modal"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||P.trigger(this._element,_n,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(Tn),this._adjustDialog(),this._backdrop.show(()=>this._showElement(t)))}hide(){this._isShown&&!this._isTransitioning&&(P.trigger(this._element,pn).defaultPrevented||(this._isShown=!1,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(Cn),this._queueCallback(()=>this._hideModal(),this._element,this._isAnimated())))}dispose(){P.off(window,fn),P.off(this._dialog,fn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Zi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new an({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0;const e=R.findOne(".modal-body",this._dialog);e&&(e.scrollTop=0),u(this._element),this._element.classList.add(Cn),this._queueCallback(()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,P.trigger(this._element,bn,{relatedTarget:t})},this._dialog,this._isAnimated())}_addEventListeners(){P.on(this._element,An,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())}),P.on(window,vn,()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()}),P.on(this._element,wn,t=>{P.one(this._element,yn,e=>{this._element===t.target&&this._element===e.target&&("static"!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())})})}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide(()=>{document.body.classList.remove(Tn),this._resetAdjustments(),this._scrollBar.reset(),P.trigger(this._element,gn)})}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(P.trigger(this._element,mn).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;"hidden"===e||this._element.classList.contains(On)||(t||(this._element.style.overflowY="hidden"),this._element.classList.add(On),this._queueCallback(()=>{this._element.classList.remove(On),this._queueCallback(()=>{this._element.style.overflowY=e},this._dialog)},this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=m()?"paddingLeft":"paddingRight";this._element.style[t]=`${e}px`}if(!i&&t){const t=m()?"paddingRight":"paddingLeft";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each(function(){const i=Ln.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}})}}P.on(document,En,'[data-bs-toggle="modal"]',function(t){const e=R.getElementFromSelector(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),P.one(e,_n,t=>{t.defaultPrevented||P.one(e,gn,()=>{l(this)&&this.focus()})});const i=R.findOne(".modal.show");i&&Ln.getInstance(i).hide(),Ln.getOrCreateInstance(e).toggle(this)}),q(Ln),g(Ln);const Sn=".bs.offcanvas",Dn=".data-api",$n=`load${Sn}${Dn}`,In="show",Nn="showing",Pn="hiding",jn=".offcanvas.show",Mn=`show${Sn}`,Fn=`shown${Sn}`,Hn=`hide${Sn}`,Wn=`hidePrevented${Sn}`,Bn=`hidden${Sn}`,zn=`resize${Sn}`,Rn=`click${Sn}${Dn}`,qn=`keydown.dismiss${Sn}`,Vn={backdrop:!0,keyboard:!0,scroll:!1},Kn={backdrop:"(boolean|string)",keyboard:"boolean",scroll:"boolean"};class Qn extends B{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return Vn}static get DefaultType(){return Kn}static get NAME(){return"offcanvas"}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||P.trigger(this._element,Mn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new un).hide(),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add(Nn),this._queueCallback(()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(In),this._element.classList.remove(Nn),P.trigger(this._element,Fn,{relatedTarget:t})},this._element,!0))}hide(){this._isShown&&(P.trigger(this._element,Hn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add(Pn),this._backdrop.hide(),this._queueCallback(()=>{this._element.classList.remove(In,Pn),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._config.scroll||(new un).reset(),P.trigger(this._element,Bn)},this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Zi({className:"offcanvas-backdrop",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{"static"!==this._config.backdrop?this.hide():P.trigger(this._element,Wn)}:null})}_initializeFocusTrap(){return new an({trapElement:this._element})}_addEventListeners(){P.on(this._element,qn,t=>{"Escape"===t.key&&(this._config.keyboard?this.hide():P.trigger(this._element,Wn))})}static jQueryInterface(t){return this.each(function(){const e=Qn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}})}}P.on(document,Rn,'[data-bs-toggle="offcanvas"]',function(t){const e=R.getElementFromSelector(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;P.one(e,Bn,()=>{l(this)&&this.focus()});const i=R.findOne(jn);i&&i!==e&&Qn.getInstance(i).hide(),Qn.getOrCreateInstance(e).toggle(this)}),P.on(window,$n,()=>{for(const t of R.find(jn))Qn.getOrCreateInstance(t).show()}),P.on(window,zn,()=>{for(const t of R.find("[aria-modal][class*=show][class*=offcanvas-]"))"fixed"!==getComputedStyle(t).position&&Qn.getOrCreateInstance(t).hide()}),q(Qn),g(Qn);const Xn={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Yn=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Un=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Gn=(t,e)=>{const i=t.nodeName.toLowerCase();return e.includes(i)?!Yn.has(i)||Boolean(Un.test(t.nodeValue)):e.filter(t=>t instanceof RegExp).some(t=>t.test(i))},Jn={allowList:Xn,content:{},extraClass:"",html:!1,sanitize:!0,sanitizeFn:null,template:"
    "},Zn={allowList:"object",content:"object",extraClass:"(string|function)",html:"boolean",sanitize:"boolean",sanitizeFn:"(null|function)",template:"string"},ts={entry:"(string|element|function|null)",selector:"(string|element)"};class es extends W{constructor(t){super(),this._config=this._getConfig(t)}static get Default(){return Jn}static get DefaultType(){return Zn}static get NAME(){return"TemplateFactory"}getContent(){return Object.values(this._config.content).map(t=>this._resolvePossibleFunction(t)).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement("div");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split(" ")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},ts)}_setContent(t,e,i){const n=R.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?r(e)?this._putElementInTemplate(a(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&"function"==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,"text/html"),s=[].concat(...n.body.querySelectorAll("*"));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e["*"]||[],e[i]||[]);for(const e of n)Gn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return _(t,[void 0,this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="",void e.append(t);e.textContent=t.textContent}}const is=new Set(["sanitize","allowList","sanitizeFn"]),ns="fade",ss="show",os=".tooltip-inner",rs=".modal",as="hide.bs.modal",ls="hover",cs="focus",hs="click",ds={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},us={allowList:Xn,animation:!0,boundary:"clippingParents",container:!1,customClass:"",delay:0,fallbackPlacements:["top","right","bottom","left"],html:!1,offset:[0,6],placement:"top",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'',title:"",trigger:"hover focus"},fs={allowList:"object",animation:"boolean",boundary:"(string|element)",container:"(string|element|boolean)",customClass:"(string|function)",delay:"(number|object)",fallbackPlacements:"array",html:"boolean",offset:"(array|string|function)",placement:"(string|function)",popperConfig:"(null|object|function)",sanitize:"boolean",sanitizeFn:"(null|function)",selector:"(string|boolean)",template:"string",title:"(string|element|function)",trigger:"string"};class ps extends B{constructor(t,e){if(void 0===Ai)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org/docs/v2/)");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners(),this._config.selector||this._fixTitle()}static get Default(){return us}static get DefaultType(){return fs}static get NAME(){return"tooltip"}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),P.off(this._element.closest(rs),as,this._hideModalHandler),this._element.getAttribute("data-bs-original-title")&&this._element.setAttribute("title",this._element.getAttribute("data-bs-original-title")),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this._isWithContent()||!this._isEnabled)return;const t=P.trigger(this._element,this.constructor.eventName("show")),e=(h(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute("aria-describedby",i.getAttribute("id"));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),P.trigger(this._element,this.constructor.eventName("inserted"))),this._popper=this._createPopper(i),i.classList.add(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.on(t,"mouseover",d);this._queueCallback(()=>{P.trigger(this._element,this.constructor.eventName("shown")),!1===this._isHovered&&this._leave(),this._isHovered=!1},this.tip,this._isAnimated())}hide(){if(this._isShown()&&!P.trigger(this._element,this.constructor.eventName("hide")).defaultPrevented){if(this._getTipElement().classList.remove(ss),"ontouchstart"in document.documentElement)for(const t of[].concat(...document.body.children))P.off(t,"mouseover",d);this._activeTrigger[hs]=!1,this._activeTrigger[cs]=!1,this._activeTrigger[ls]=!1,this._isHovered=null,this._queueCallback(()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute("aria-describedby"),P.trigger(this._element,this.constructor.eventName("hidden")))},this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ns,ss),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME).toString();return e.setAttribute("id",i),this._isAnimated()&&e.classList.add(ns),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new es({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{[os]:this._getTitle()}}_getTitle(){return this._resolvePossibleFunction(this._config.title)||this._element.getAttribute("data-bs-original-title")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ns)}_isShown(){return this.tip&&this.tip.classList.contains(ss)}_createPopper(t){const e=_(this._config.placement,[this,t,this._element]),i=ds[e.toUpperCase()];return wi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return _(t,[this._element,this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"preSetPlacement",enabled:!0,phase:"beforeMain",fn:t=>{this._getTipElement().setAttribute("data-popper-placement",t.state.placement)}}]};return{...e,..._(this._config.popperConfig,[void 0,e])}}_setListeners(){const t=this._config.trigger.split(" ");for(const e of t)if("click"===e)P.on(this._element,this.constructor.eventName("click"),this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[hs]=!(e._isShown()&&e._activeTrigger[hs]),e.toggle()});else if("manual"!==e){const t=e===ls?this.constructor.eventName("mouseenter"):this.constructor.eventName("focusin"),i=e===ls?this.constructor.eventName("mouseleave"):this.constructor.eventName("focusout");P.on(this._element,t,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusin"===t.type?cs:ls]=!0,e._enter()}),P.on(this._element,i,this._config.selector,t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger["focusout"===t.type?cs:ls]=e._element.contains(t.relatedTarget),e._leave()})}this._hideModalHandler=()=>{this._element&&this.hide()},P.on(this._element.closest(rs),as,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute("title");t&&(this._element.getAttribute("aria-label")||this._element.textContent.trim()||this._element.setAttribute("aria-label",t),this._element.setAttribute("data-bs-original-title",t),this._element.removeAttribute("title"))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout(()=>{this._isHovered&&this.show()},this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1,this._setTimeout(()=>{this._isHovered||this.hide()},this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=H.getDataAttributes(this._element);for(const t of Object.keys(e))is.has(t)&&delete e[t];return t={...e,..."object"==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:a(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger="manual",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each(function(){const e=ps.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}g(ps);const ms=".popover-header",gs=".popover-body",_s={...ps.Default,content:"",offset:[0,8],placement:"right",template:'',trigger:"click"},bs={...ps.DefaultType,content:"(null|string|element|function)"};class vs extends ps{static get Default(){return _s}static get DefaultType(){return bs}static get NAME(){return"popover"}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{[ms]:this._getTitle(),[gs]:this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each(function(){const e=vs.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}})}}g(vs);const ys=".bs.scrollspy",ws=`activate${ys}`,As=`click${ys}`,Es=`load${ys}.data-api`,Ts="active",Cs="[href]",Os=".nav-link",xs=`${Os}, .nav-item > ${Os}, .list-group-item`,ks={offset:null,rootMargin:"0px 0px -25%",smoothScroll:!1,target:null,threshold:[.1,.5,1]},Ls={offset:"(number|null)",rootMargin:"string",smoothScroll:"boolean",target:"element",threshold:"array"};class Ss extends B{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement="visible"===getComputedStyle(this._element).overflowY?null:this._element,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ks}static get DefaultType(){return Ls}static get NAME(){return"scrollspy"}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect(),super.dispose()}_configAfterMerge(t){return t.target=a(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,"string"==typeof t.threshold&&(t.threshold=t.threshold.split(",").map(t=>Number.parseFloat(t))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(P.off(this._config.target,As),P.on(this._config.target,As,Cs,t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop;if(i.scrollTo)return void i.scrollTo({top:n,behavior:"smooth"});i.scrollTop=n}}))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver(t=>this._observerCallback(t),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=R.find(Cs,this._config.target);for(const e of t){if(!e.hash||c(e))continue;const t=R.findOne(decodeURI(e.hash),this._element);l(t)&&(this._targetLinks.set(decodeURI(e.hash),e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(Ts),this._activateParents(t),P.trigger(this._element,ws,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains("dropdown-item"))R.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(Ts);else for(const e of R.parents(t,".nav, .list-group"))for(const t of R.prev(e,xs))t.classList.add(Ts)}_clearActiveClass(t){t.classList.remove(Ts);const e=R.find(`${Cs}.${Ts}`,t);for(const t of e)t.classList.remove(Ts)}static jQueryInterface(t){return this.each(function(){const e=Ss.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}P.on(window,Es,()=>{for(const t of R.find('[data-bs-spy="scroll"]'))Ss.getOrCreateInstance(t)}),g(Ss);const Ds=".bs.tab",$s=`hide${Ds}`,Is=`hidden${Ds}`,Ns=`show${Ds}`,Ps=`shown${Ds}`,js=`click${Ds}`,Ms=`keydown${Ds}`,Fs=`load${Ds}`,Hs="ArrowLeft",Ws="ArrowRight",Bs="ArrowUp",zs="ArrowDown",Rs="Home",qs="End",Vs="active",Ks="fade",Qs="show",Xs=".dropdown-toggle",Ys=`:not(${Xs})`,Us='[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',Gs=`.nav-link${Ys}, .list-group-item${Ys}, [role="tab"]${Ys}, ${Us}`,Js=`.${Vs}[data-bs-toggle="tab"], .${Vs}[data-bs-toggle="pill"], .${Vs}[data-bs-toggle="list"]`;class Zs extends B{constructor(t){super(t),this._parent=this._element.closest('.list-group, .nav, [role="tablist"]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),P.on(this._element,Ms,t=>this._keydown(t)))}static get NAME(){return"tab"}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?P.trigger(e,$s,{relatedTarget:t}):null;P.trigger(t,Ns,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Vs),this._activate(R.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.removeAttribute("tabindex"),t.setAttribute("aria-selected",!0),this._toggleDropDown(t,!0),P.trigger(t,Ps,{relatedTarget:e})):t.classList.add(Qs)},t,t.classList.contains(Ks)))}_deactivate(t,e){t&&(t.classList.remove(Vs),t.blur(),this._deactivate(R.getElementFromSelector(t)),this._queueCallback(()=>{"tab"===t.getAttribute("role")?(t.setAttribute("aria-selected",!1),t.setAttribute("tabindex","-1"),this._toggleDropDown(t,!1),P.trigger(t,Is,{relatedTarget:e})):t.classList.remove(Qs)},t,t.classList.contains(Ks)))}_keydown(t){if(![Hs,Ws,Bs,zs,Rs,qs].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter(t=>!c(t));let i;if([Rs,qs].includes(t.key))i=e[t.key===Rs?0:e.length-1];else{const n=[Ws,zs].includes(t.key);i=v(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Zs.getOrCreateInstance(i).show())}_getChildren(){return R.find(Gs,this._parent)}_getActiveElem(){return this._getChildren().find(t=>this._elemIsActive(t))||null}_setInitialAttributes(t,e){this._setAttributeIfNotExists(t,"role","tablist");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute("aria-selected",e),i!==t&&this._setAttributeIfNotExists(i,"role","presentation"),e||t.setAttribute("tabindex","-1"),this._setAttributeIfNotExists(t,"role","tab"),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=R.getElementFromSelector(t);e&&(this._setAttributeIfNotExists(e,"role","tabpanel"),t.id&&this._setAttributeIfNotExists(e,"aria-labelledby",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains("dropdown"))return;const n=(t,n)=>{const s=R.findOne(t,i);s&&s.classList.toggle(n,e)};n(Xs,Vs),n(".dropdown-menu",Qs),i.setAttribute("aria-expanded",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Vs)}_getInnerElement(t){return t.matches(Gs)?t:R.findOne(Gs,t)}_getOuterElement(t){return t.closest(".nav-item, .list-group-item")||t}static jQueryInterface(t){return this.each(function(){const e=Zs.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t]()}})}}P.on(document,js,Us,function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||Zs.getOrCreateInstance(this).show()}),P.on(window,Fs,()=>{for(const t of R.find(Js))Zs.getOrCreateInstance(t)}),g(Zs);const to=".bs.toast",eo=`mouseover${to}`,io=`mouseout${to}`,no=`focusin${to}`,so=`focusout${to}`,oo=`hide${to}`,ro=`hidden${to}`,ao=`show${to}`,lo=`shown${to}`,co="hide",ho="show",uo="showing",fo={animation:"boolean",autohide:"boolean",delay:"number"},po={animation:!0,autohide:!0,delay:5e3};class mo extends B{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get Default(){return po}static get DefaultType(){return fo}static get NAME(){return"toast"}show(){P.trigger(this._element,ao).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(co),u(this._element),this._element.classList.add(ho,uo),this._queueCallback(()=>{this._element.classList.remove(uo),P.trigger(this._element,lo),this._maybeScheduleHide()},this._element,this._config.animation))}hide(){this.isShown()&&(P.trigger(this._element,oo).defaultPrevented||(this._element.classList.add(uo),this._queueCallback(()=>{this._element.classList.add(co),this._element.classList.remove(uo,ho),P.trigger(this._element,ro)},this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(ho),super.dispose()}isShown(){return this._element.classList.contains(ho)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout(()=>{this.hide()},this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){P.on(this._element,eo,t=>this._onInteraction(t,!0)),P.on(this._element,io,t=>this._onInteraction(t,!1)),P.on(this._element,no,t=>this._onInteraction(t,!0)),P.on(this._element,so,t=>this._onInteraction(t,!1))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each(function(){const e=mo.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}})}}return q(mo),g(mo),{Alert:X,Button:U,Carousel:St,Collapse:qt,Dropdown:Qi,Modal:Ln,Offcanvas:Qn,Popover:vs,ScrollSpy:Ss,Tab:Zs,Toast:mo,Tooltip:ps}}); //# sourceMappingURL=bootstrap.bundle.min.js.map \ No newline at end of file diff --git a/data/web/js/build/013-mailcow.js b/data/web/js/build/013-mailcow.js index d897f23ea..9586af33d 100644 --- a/data/web/js/build/013-mailcow.js +++ b/data/web/js/build/013-mailcow.js @@ -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'); } @@ -345,15 +348,15 @@ $(document).ready(function() { $('.main-logo-dark').addClass('d-none'); if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_dark.png'); if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_dark.png'); - localStorage.setItem('theme', 'light'); + localStorage.setItem('mailcow_theme', 'light'); }else{ - $('head').append(''); + $('head').append(''); $('#dark-mode-toggle').prop('checked', true); $('.main-logo').addClass('d-none'); $('.main-logo-dark').removeClass('d-none'); if ($('#rspamd_logo').length) $('#rspamd_logo').attr('src', '/img/rspamd_logo_light.png'); if ($('#rspamd_logo_sm').length) $('#rspamd_logo_sm').attr('src', '/img/rspamd_logo_light.png'); - localStorage.setItem('theme', 'dark'); + localStorage.setItem('mailcow_theme', 'dark'); } } diff --git a/data/web/js/site/dashboard.js b/data/web/js/site/dashboard.js index 760e878f1..7c077e27b 100644 --- a/data/web/js/site/dashboard.js +++ b/data/web/js/site/dashboard.js @@ -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('
    \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); @@ -1128,6 +1130,11 @@ jQuery(function($){ item.ua = escapeHtml(item.ua); } item.ua = '' + item.ua + ''; + if (item.user == null) { + item.user = 'unknown'; + } else { + item.user = escapeHtml(item.user); + } if (item.service == "activesync") { item.service = 'ActiveSync'; } @@ -1162,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 = '' + item.task + ''; item.type = '' + item.type + ''; }); @@ -1169,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 = '
    ' + item.service.toUpperCase() + '
    '; }); } else if (table == 'general_syslog') { @@ -1207,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"; } @@ -1664,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("" + lang_debug.no_update_available + ""); - } else { - // update available - $("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning"); - $("#mailcow_update").html(lang_debug.update_available + ` `+latest_data.tag_name+``); - $("#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("" + lang_debug.no_update_available + ""); + } else if (data.status === "update_available") { + $("#mailcow_update").removeClass("text-danger text-success").addClass("text-warning"); + $("#mailcow_update").html(lang_debug.update_available + ` `+data.latest_tag+``); + $("#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(""+ lang_debug.update_failed +""); - }); + showVersionModal("New Release " + data.latest_tag, data.latest_tag); + }) + } else { + throw new Error(data.message || "update check failed"); + } }).catch(err => { // err console.log(err); diff --git a/data/web/js/site/index.js b/data/web/js/site/index.js index 4c812a377..29b72f0d5 100644 --- a/data/web/js/site/index.js +++ b/data/web/js/site/index.js @@ -1,5 +1,6 @@ $(document).ready(function() { - var theme = localStorage.getItem("theme"); - localStorage.clear(); - localStorage.setItem("theme", theme); + var theme = localStorage.getItem("mailcow_theme"); + if (theme !== null) { + localStorage.setItem("mailcow_theme", theme); + } }); diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 7010077db..5ad5cceab 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -424,6 +424,11 @@ $(document).ready(function() { } else { $('#force_pw_update').prop('checked', false); } + if (template.force_tfa == 1){ + $('#force_tfa').prop('checked', true); + } else { + $('#force_tfa').prop('checked', false); + } if (template.sogo_access == 1){ $('#sogo_access').prop('checked', true); } else { @@ -471,8 +476,13 @@ jQuery(function($){ hideTableExpandCollapseBtn('#tab-domains', '#domain_table'); }, ajax: { - type: "GET", - url: "/api/v1/get/domain/datatables", + type: "POST", + url: "/api/v1/search/domain", + contentType: "application/json", + processData: false, + data: function(d) { + return JSON.stringify(d); + }, dataSrc: function(json){ $.each(json.data, function(i, item) { item.domain_name = escapeHtml(item.domain_name); @@ -898,8 +908,13 @@ jQuery(function($){ hideTableExpandCollapseBtn('#tab-mailboxes', '#mailbox_table'); }, ajax: { - type: "GET", - url: "/api/v1/get/mailbox/datatables", + type: "POST", + url: "/api/v1/search/mailbox", + contentType: "application/json", + processData: false, + data: function(d) { + return JSON.stringify(d); + }, dataSrc: function(json){ $.each(json.data, function (i, item) { item.quota = { @@ -981,6 +996,7 @@ jQuery(function($){ 'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '' }; item.username = escapeHtml(item.username); + item.name = escapeHtml(item.name); if (Array.isArray(item.tags)){ var tags = ''; @@ -1232,6 +1248,7 @@ jQuery(function($){ item.attributes.eas_access = '' + (item.attributes.eas_access == 1 ? '1' : '0') + ''; item.attributes.dav_access = '' + (item.attributes.dav_access == 1 ? '1' : '0') + ''; item.attributes.sogo_access = '' + (item.attributes.sogo_access == 1 ? '1' : '0') + ''; + item.attributes.force_tfa = '' + (item.attributes.force_tfa == 1 ? '1' : '0') + ''; if (item.attributes.quarantine_notification === 'never') { item.attributes.quarantine_notification = lang.never; } else if (item.attributes.quarantine_notification === 'hourly') { @@ -1375,6 +1392,11 @@ jQuery(function($){ return 1==data?'':''; } }, + { + title: lang.force_tfa, + data: 'attributes.force_tfa', + defaultContent: '' + }, { title: lang_edit.ratelimit, data: 'attributes.ratelimit', diff --git a/data/web/js/site/quarantine.js b/data/web/js/site/quarantine.js index fbf4fe862..a9b7845bc 100644 --- a/data/web/js/site/quarantine.js +++ b/data/web/js/site/quarantine.js @@ -102,18 +102,21 @@ jQuery(function($){ { title: 'ID', data: 'id', - defaultContent: '' + defaultContent: '', + render: $.fn.dataTable.render.text() }, { title: lang.qid, data: 'qid', - defaultContent: '' + defaultContent: '', + render: $.fn.dataTable.render.text() }, { title: lang.sender, data: 'sender', className: 'senders-mw220', - defaultContent: '' + defaultContent: '', + render: $.fn.dataTable.render.text() }, { title: lang.subj, @@ -128,7 +131,8 @@ jQuery(function($){ { title: lang.rcpt, data: 'rcpt', - defaultContent: '' + defaultContent: '', + render: $.fn.dataTable.render.text() }, { title: lang.danger, @@ -226,18 +230,18 @@ jQuery(function($){ } if (typeof data.fuzzy_hashes === 'object' && data.fuzzy_hashes !== null && data.fuzzy_hashes.length !== 0) { $.each(data.fuzzy_hashes, function (index, value) { - $('#qid_detail_fuzzy').append('

    ' + value + '

    '); + $('#qid_detail_fuzzy').append('

    ' + escapeHtml(value) + '

    '); }); } else { $('#qid_detail_fuzzy').append('-'); } if (typeof data.score !== 'undefined' && typeof data.action !== 'undefined') { if (data.action == "add header") { - $('#qid_detail_score').append('' + data.score + ' - ' + lang.junk_folder + ''); + $('#qid_detail_score').append('' + escapeHtml(data.score) + ' - ' + lang.junk_folder + ''); } else if (data.action == "reject") { - $('#qid_detail_score').append('' + data.score + ' - ' + lang.rejected + ''); + $('#qid_detail_score').append('' + escapeHtml(data.score) + ' - ' + lang.rejected + ''); } else if (data.action == "rewrite subject") { - $('#qid_detail_score').append('' + data.score + ' - ' + lang.rewrite_subject + ''); + $('#qid_detail_score').append('' + escapeHtml(data.score) + ' - ' + lang.rewrite_subject + ''); } } if (typeof data.recipients !== 'undefined') { @@ -254,8 +258,8 @@ jQuery(function($){ qAtts.text(''); $.each(data.attachments, function(index, value) { qAtts.append( - '

    ' + value[0] + ' (' + value[1] + ')' + - ' - ' + lang.check_hash + '

    ' + '

    ' + escapeHtml(value[0]) + ' (' + escapeHtml(value[1]) + ')' + + ' - ' + lang.check_hash + '

    ' ); }); } diff --git a/data/web/js/site/queue.js b/data/web/js/site/queue.js index 26c15091d..9adc79b5f 100644 --- a/data/web/js/site/queue.js +++ b/data/web/js/site/queue.js @@ -48,13 +48,13 @@ jQuery(function($){ url: "/api/v1/get/mailq/all", dataSrc: function(data){ $.each(data, function (i, item) { - item.chkbox = ''; + item.chkbox = ''; rcpts = $.map(item.recipients, function(i) { return escapeHtml(i); }); item.recipients = rcpts.join('
    '); item.action = ''; }); return data; @@ -79,12 +79,14 @@ jQuery(function($){ { title: 'QID', data: 'queue_id', - defaultContent: '' + defaultContent: '', + render: $.fn.dataTable.render.text() }, { title: 'Queue', data: 'queue_name', - defaultContent: '' + defaultContent: '', + render: $.fn.dataTable.render.text() }, { title: lang_admin.arrival_time, @@ -106,7 +108,8 @@ jQuery(function($){ { title: lang_admin.sender, data: 'sender', - defaultContent: '' + defaultContent: '', + render: $.fn.dataTable.render.text() }, { title: lang_admin.recipients, diff --git a/data/web/js/site/user.js b/data/web/js/site/user.js index 5eecf2080..288e7abd2 100644 --- a/data/web/js/site/user.js +++ b/data/web/js/site/user.js @@ -98,8 +98,8 @@ jQuery(function($){ var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"}); var service = '
    ' + item.service.toUpperCase() + '
    '; var app_password = item.app_password ? ' ' + escapeHtml(item.app_password_name || "App") + '' : ''; - var real_rip = item.real_rip.startsWith("Web") ? item.real_rip : '' + item.real_rip + ""; - var ip_location = item.location ? ' ' : ''; + var real_rip = item.real_rip.startsWith("Web") ? escapeHtml(item.real_rip) : '' + escapeHtml(item.real_rip) + ""; + var ip_location = item.location ? ' ' : ''; var ip_data = real_rip + ip_location + app_password; $(".last-sasl-login").append(` diff --git a/data/web/json_api.php b/data/web/json_api.php index 5409e65d3..fc34b2595 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -91,6 +91,11 @@ if (isset($_GET['query'])) { if ($action == 'delete') { $_POST['items'] = $request; } + + // search + if ($action == 'search') { + // placeholder for search, as the request body is already decoded and available in $requestDecoded + } } api_log($_POST); @@ -169,6 +174,8 @@ if (isset($_GET['query'])) { exit; } fido2(array("action" => "register", "registration" => $data)); + // Release pending_tfa_setup session hold + unset($_SESSION['pending_tfa_setup']); $return = new stdClass(); $return->success = true; echo json_encode($return); @@ -457,47 +464,6 @@ if (isset($_GET['query'])) { case "domain": switch ($object) { - case "datatables": - $table = ['domain', 'd']; - $primaryKey = 'domain'; - $columns = [ - ['db' => 'domain', 'dt' => 2], - ['db' => 'aliases', 'dt' => 3, 'order_subquery' => "SELECT COUNT(*) FROM `alias` WHERE (`domain`= `d`.`domain` OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = `d`.`domain`)) AND `address` NOT IN (SELECT `username` FROM `mailbox`)"], - ['db' => 'mailboxes', 'dt' => 4, 'order_subquery' => "SELECT COUNT(*) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"], - ['db' => 'quota', 'dt' => 5, 'order_subquery' => "SELECT COALESCE(SUM(`mailbox`.`quota`), 0) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"], - ['db' => 'stats', 'dt' => 6, 'dummy' => true, 'order_subquery' => "SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` IN (SELECT `username` FROM `mailbox` WHERE `domain` = `d`.`domain`)"], - ['db' => 'defquota', 'dt' => 7], - ['db' => 'maxquota', 'dt' => 8], - ['db' => 'backupmx', 'dt' => 10], - ['db' => 'tags', 'dt' => 14, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_domain` AS `td` ON `td`.`domain` = `d`.`domain`', 'where_column' => '`td`.`tag_name`']], - ['db' => 'active', 'dt' => 15], - ]; - - require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php'; - global $pdo; - if($_SESSION['mailcow_cc_role'] === 'admin') { - $data = SSP::simple($_GET, $pdo, $table, $primaryKey, $columns); - } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') { - $data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns, - 'INNER JOIN domain_admins as da ON da.domain = d.domain', - [ - 'condition' => 'da.active = 1 and da.username = :username', - 'bindings' => ['username' => $_SESSION['mailcow_cc_username']] - ]); - } - - if (!empty($data['data'])) { - $domainsData = []; - foreach ($data['data'] as $domain) { - if ($details = mailbox('get', 'domain_details', $domain[2])) { - $domainsData[] = $details; - } - } - $data['data'] = $domainsData; - } - - process_get_return($data); - break; case "all": $tags = null; if (isset($_GET['tags']) && $_GET['tags'] != '') @@ -997,46 +963,6 @@ if (isset($_GET['query'])) { break; case "mailbox": switch ($object) { - case "datatables": - $table = ['mailbox', 'm']; - $primaryKey = 'username'; - $columns = [ - ['db' => 'username', 'dt' => 2], - ['db' => 'quota', 'dt' => 3], - ['db' => 'last_mail_login', 'dt' => 4, 'dummy' => true, 'order_subquery' => "SELECT MAX(`datetime`) FROM `sasl_log` WHERE `service` != 'SSO' AND `username` = `m`.`username`"], - ['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"], - ['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"], - ['db' => 'name', 'dt' => 7], - ['db' => 'messages', 'dt' => 20, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"], - ['db' => 'tags', 'dt' => 23, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']], - ['db' => 'active', 'dt' => 24], - ]; - - require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php'; - global $pdo; - if($_SESSION['mailcow_cc_role'] === 'admin') { - $data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns, null, "(`m`.`kind` = '' OR `m`.`kind` = NULL)"); - } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') { - $data = SSP::complex($_GET, $pdo, $table, $primaryKey, $columns, - 'INNER JOIN domain_admins as da ON da.domain = m.domain', - [ - 'condition' => "(`m`.`kind` = '' OR `m`.`kind` = NULL) AND `da`.`active` = 1 AND `da`.`username` = :username", - 'bindings' => ['username' => $_SESSION['mailcow_cc_username']] - ]); - } - - if (!empty($data['data'])) { - $mailboxData = []; - foreach ($data['data'] as $mailbox) { - if ($details = mailbox('get', 'mailbox_details', $mailbox[2])) { - $mailboxData[] = $details; - } - } - $data['data'] = $mailboxData; - } - - process_get_return($data); - break; case "all": case "reduced": $tags = null; @@ -1625,6 +1551,136 @@ if (isset($_GET['query'])) { } } break; + case "search": + function process_search_return($return) { + if ($return === false) { + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Cannot get item' + )); + } + else { + echo json_encode($return, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + // only allow POST requests to SEARCH API endpoints + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + http_response_code(405); + echo json_encode(array( + 'type' => 'error', + 'msg' => 'only POST method is allowed' + )); + exit(); + } + + // Load SSP class + require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/lib/ssp.class.php'; + global $pdo; + + switch ($category) { + case "domain": + $table = ['domain', 'd']; + $primaryKey = 'domain'; + $columns = [ + ['db' => 'domain', 'dt' => 2], + ['db' => 'aliases', 'dt' => 3, 'order_subquery' => "SELECT COUNT(*) FROM `alias` WHERE (`domain`= `d`.`domain` OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = `d`.`domain`)) AND `address` NOT IN (SELECT `username` FROM `mailbox`)"], + ['db' => 'mailboxes', 'dt' => 4, 'order_subquery' => "SELECT COUNT(*) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"], + ['db' => 'quota', 'dt' => 5, 'order_subquery' => "SELECT COALESCE(SUM(`mailbox`.`quota`), 0) FROM `mailbox` WHERE `mailbox`.`domain` = `d`.`domain` AND (`mailbox`.`kind` = '' OR `mailbox`.`kind` = NULL)"], + ['db' => 'stats', 'dt' => 6, 'dummy' => true, 'order_subquery' => "SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` IN (SELECT `username` FROM `mailbox` WHERE `domain` = `d`.`domain`)"], + ['db' => 'defquota', 'dt' => 7], + ['db' => 'maxquota', 'dt' => 8], + ['db' => 'backupmx', 'dt' => 10], + ['db' => 'tags', 'dt' => 14, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_domain` AS `td` ON `td`.`domain` = `d`.`domain`', 'where_column' => '`td`.`tag_name`']], + ['db' => 'active', 'dt' => 15], + ]; + + if($_SESSION['mailcow_cc_role'] === 'admin') { + $data = SSP::simple($requestDecoded, $pdo, $table, $primaryKey, $columns); + } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') { + $data = SSP::complex($requestDecoded, $pdo, $table, $primaryKey, $columns, + 'INNER JOIN domain_admins as da ON da.domain = d.domain', + [ + 'condition' => 'da.active = 1 and da.username = :username', + 'bindings' => ['username' => $_SESSION['mailcow_cc_username']] + ]); + } else { + http_response_code(403); + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Insufficient permissions' + )); + exit(); + } + + if (!empty($data['data'])) { + $domainsData = []; + foreach ($data['data'] as $domain) { + if ($details = mailbox('get', 'domain_details', $domain[2])) { + $domainsData[] = $details; + } + } + $data['data'] = $domainsData; + } + + process_search_return($data); + break; + + case "mailbox": + $table = ['mailbox', 'm']; + $primaryKey = 'username'; + $columns = [ + ['db' => 'username', 'dt' => 2], + ['db' => 'quota', 'dt' => 3], + ['db' => 'last_mail_login', 'dt' => 4, 'dummy' => true, 'order_subquery' => "SELECT MAX(`datetime`) FROM `sasl_log` WHERE `service` != 'SSO' AND `username` = `m`.`username`"], + ['db' => 'last_pw_change', 'dt' => 5, 'dummy' => true, 'order_subquery' => "JSON_EXTRACT(attributes, '$.passwd_update')"], + ['db' => 'in_use', 'dt' => 6, 'dummy' => true, 'order_subquery' => "(SELECT SUM(bytes) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`) / `m`.`quota`"], + ['db' => 'name', 'dt' => 7], + ['db' => 'messages', 'dt' => 20, 'dummy' => true, 'order_subquery' => "SELECT SUM(messages) FROM `quota2` WHERE `quota2`.`username` = `m`.`username`"], + ['db' => 'tags', 'dt' => 23, 'dummy' => true, 'search' => ['join' => 'LEFT JOIN `tags_mailbox` AS `tm` ON `tm`.`username` = `m`.`username`', 'where_column' => '`tm`.`tag_name`']], + ['db' => 'active', 'dt' => 24], + ]; + + if($_SESSION['mailcow_cc_role'] === 'admin') { + $data = SSP::complex($requestDecoded, $pdo, $table, $primaryKey, $columns, null, + "(`m`.`kind` = '' OR `m`.`kind` = NULL)"); + } elseif ($_SESSION['mailcow_cc_role'] === 'domainadmin') { + $data = SSP::complex($requestDecoded, $pdo, $table, $primaryKey, $columns, + 'INNER JOIN domain_admins as da ON da.domain = m.domain', + [ + 'condition' => "(`m`.`kind` = '' OR `m`.`kind` = NULL) AND `da`.`active` = 1 AND `da`.`username` = :username", + 'bindings' => ['username' => $_SESSION['mailcow_cc_username']] + ]); + } else { + http_response_code(403); + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Insufficient permissions' + )); + exit(); + } + + if (!empty($data['data'])) { + $mailboxData = []; + foreach ($data['data'] as $mailbox) { + if ($details = mailbox('get', 'mailbox_details', $mailbox[2])) { + $mailboxData[] = $details; + } + } + $data['data'] = $mailboxData; + } + + process_search_return($data); + break; + + default: + http_response_code(404); + echo json_encode(array( + 'type' => 'error', + 'msg' => 'Invalid search category' + )); + break; + } + break; case "delete": if ($_SESSION['mailcow_cc_api_access'] == 'ro' || isset($_SESSION['pending_mailcow_cc_username']) || !isset($_SESSION["mailcow_cc_username"])) { http_response_code(403); @@ -1982,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)); diff --git a/data/web/lang/lang.az-az.json b/data/web/lang/lang.az-az.json new file mode 100644 index 000000000..958ed8e2a --- /dev/null +++ b/data/web/lang/lang.az-az.json @@ -0,0 +1,17 @@ +{ + "acl": { + "login_as": "E-poçt qutusu istifadəçisi olaraq daxil ol", + "alias_domains": "Alternativ domenlər əlavə et", + "app_passwds": "Tətbiq parollarını idarə et", + "bcc_maps": "BCC yönləndirmə xəritələri", + "delimiter_action": "Ayırıcı əməliyyatı", + "domain_desc": "Domen təsvirini dəyiş", + "domain_relayhost": "Domen üçün relay serveri dəyiş", + "eas_reset": "EAS cihazlarını sıfırla", + "extend_sender_acl": "Göndərən ACL-ni xarici ünvanlarla genişləndirməyə icazə ver", + "filters": "Filtrlər", + "mailbox_relayhost": "E-poçt qutusu üçün relay serveri dəyiş", + "prohibited": "ACL tərəfindən məhdudlaşdırılıb", + "protocol_access": "Protokol girişini dəyiş" + } +} diff --git a/data/web/lang/lang.cs-cz.json b/data/web/lang/lang.cs-cz.json index b178720a9..708de8486 100644 --- a/data/web/lang/lang.cs-cz.json +++ b/data/web/lang/lang.cs-cz.json @@ -111,7 +111,8 @@ "tags": "Štítky", "dry": "Simulovat synchronizaci", "internal": "Interní", - "internal_info": "Interní aliasy jsou přístupné jen z vlastních domén nebo jejich aliasů." + "internal_info": "Interní aliasy jsou přístupné jen z vlastních domén nebo jejich aliasů.", + "sender_allowed": "Povolit jako alias pro odesílání" }, "admin": { "access": "Přístupy", @@ -304,7 +305,7 @@ "rspamd_com_settings": "Název nastavení se vygeneruje automaticky, viz ukázky nastavení níže. Více informací viz Rspamd dokumentace", "rspamd_global_filters": "Mapa globálních filtrů", "rspamd_global_filters_agree": "Budu opatrný!", - "rspamd_global_filters_info": "Mapa globálních filtrů obsahuje různé seznamy povolených a zakázaných serverů", + "rspamd_global_filters_info": "Mapa globálních filtrů obsahuje různé seznamy povolených a zakázaných serverů.", "rspamd_global_filters_regex": "Názvy stačí k vysvětlení. Položky musejí obsahovat jen platné regulární výrazy ve tvaru \"/vyraz/parametry\" (e.g. /.+@domena\\.tld/i).
    \n Každý výraz bude podroben základní kontrole, přesto je možné Rspamd 'rozbít', nebude-li syntax zcela korektní.
    \n Rspamd se pokusí po každé změně načíst mapu znovu. V případě potíží restartujte Rspamd, aby se konfigurace načetla explicitně.", "rspamd_settings_map": "Nastavení Rspamd", "sal_level": "Úroveň 'Moo'", @@ -780,7 +781,9 @@ "mta_sts_max_age_info": "Doba v sekundách, po niž poštovní servery mohou toho pravidlo držet v mezipaměti bez nutnosti obnovení.", "mta_sts_mx": "Server MX", "mta_sts_mx_info": "Dovoluje odesílání jen výslovně vypsaným poštovním serverům; odesílající server kontroluje, že server MX určený v DNS odpovídá pravidlu, a povolí doručení jen s platným certifikátem TLS (chrání přes útokem typu MITM).", - "mta_sts_mx_notice": "Lze zadat více serverů MX (oddělte čárkou)." + "mta_sts_mx_notice": "Lze zadat více serverů MX (oddělte čárkou).", + "sender_allowed": "Povolit jako alias pro odesílání", + "sender_allowed_info": "Není-li povoleno, lze alias použít jen pro příjem pošty. Lze také obejít v Sender ACL a nastavit konkrétní schránky, jež tento alias mohou používat" }, "fido2": { "confirm": "Potvrdit", diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index b86879ded..67f78e049 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -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)", @@ -13,7 +14,7 @@ "mailbox_relayhost": "Relayhost für eine Mailbox setzen", "prohibited": "Untersagt durch Richtlinie", "protocol_access": "Ändern der erlaubten Protokolle", - "pushover": "Pushover", + "pushover": "Pushover-Berechtigung", "pw_reset": "Verwalten der E-Mail zur Passwortwiederherstellung erlauben", "quarantine": "Quarantäne-Aktionen", "quarantine_attachments": "Anhänge aus Quarantäne", @@ -96,7 +97,7 @@ "relay_transport_info": "
    Info
    Transport-Maps können erstellt werden, um individuelle Ziele für eine Relay-Domain zu definieren.", "relay_unknown_only": "Nur nicht-lokale Mailboxen relayen. Existente Mailboxen werden weiterhin lokal zugestellt.", "relayhost_wrapped_tls_info": "Bitte keine \"TLS-wrapped Ports\" verwenden (etwa SMTPS via Port 465/tcp).
    \r\nDer Transport wird stattdessen STARTTLS anfordern, um TLS zu verwenden. TLS kann unter \"TLS Policy Maps\" erzwungen werden.", - "select": "Bitte auswählen", + "select": "Bitte auswählen...", "select_domain": "Bitte zuerst eine Domain auswählen", "sieve_desc": "Kurze Beschreibung", "sieve_type": "Filtertyp", @@ -150,7 +151,7 @@ "arrival_time": "Ankunftszeit (Serverzeit)", "authed_user": "Auth. Benutzer", "ays": "Soll der Vorgang wirklich ausgeführt werden?", - "ban_list_info": "Übersicht ausgesperrter Netzwerke: Netzwerk (verbleibende Bannzeit) - [Aktionen].
    IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.
    Rote Labels sind Indikatoren für aktive Allowlist-Einträge.", + "ban_list_info": "Übersicht ausgesperrter Netzwerke: Netzwerk (verbleibende Bannzeit) - [Aktionen].
    IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Banns nach wenigen Sekunden.
    Rote Labels kennzeichnen aktive permanente Sperren durch Denylisting.", "change_logo": "Logo ändern", "configuration": "Konfiguration", "convert_html_to_text": "Konvertiere HTML zu reinem Text", @@ -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 data/web/css/themes/<name>/ mit folgenden Dateien:
    • theme.css (erforderlich)
    • dark.css (optional, Darkmode)
    • bootstrap.css (optional, ersetzt das Bootstrap-Basistheme)
    • theme.json (optional, name und description)
    ", + "ui_theme_none": "Keine Themes gefunden.", "unban_pending": "ausstehend", "unchanged_if_empty": "Unverändert, wenn leer", "upload": "Hochladen", @@ -431,7 +437,7 @@ "comment_too_long": "Kommentarfeld darf maximal 160 Zeichen enthalten", "cors_invalid_method": "Allow-Methods enthält eine ungültige Methode", "cors_invalid_origin": "Allow-Origins enthält eine ungültige Origin", - "defquota_empty": "Standard-Quota darf nicht 0 sein", + "defquota_empty": "Standard-Quota darf nicht 0 sein.", "demo_mode_enabled": "Demo Mode ist aktiviert", "description_invalid": "Ressourcenbeschreibung für %s ist ungültig", "dkim_domain_or_sel_exists": "Ein DKIM-Key für die Domain \"%s\" existiert und wird nicht überschrieben", @@ -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", @@ -455,13 +462,13 @@ "global_map_write_error": "Kann globale Map ID %s nicht schreiben: %s", "goto_empty": "Eine Alias-Adresse muss auf mindestens eine gültige Ziel-Adresse zeigen", "goto_invalid": "Ziel-Adresse %s ist ungültig", - "ham_learn_error": "Ham Lernfehler: %s", + "ham_learn_error": "Ham-Lernfehler: %s", "iam_test_connection": "Verbindung fehlgeschlagen", "imagick_exception": "Fataler Bildverarbeitungsfehler", "img_dimensions_exceeded": "Grafik überschreitet die maximale Bildgröße", "img_invalid": "Grafik konnte nicht validiert werden", "img_size_exceeded": "Grafik überschreitet die maximale Dateigröße", - "img_tmp_missing": "Grafik konnte nicht validiert werden: Erstellung temporärer Datei fehlgeschlagen.", + "img_tmp_missing": "Grafik konnte nicht validiert werden: Temporärer Datei nicht gefunden.", "invalid_bcc_map_type": "Ungültiger BCC-Map-Typ", "invalid_destination": "Ziel-Format \"%s\" ist ungültig", "invalid_filter_type": "Ungültiger Filtertyp", @@ -474,7 +481,7 @@ "invalid_reset_token": "Ungültiger Rücksetz-Token", "ip_list_empty": "Liste erlaubter IPs darf nicht leer sein", "is_alias": "%s lautet bereits eine Alias-Adresse", - "is_alias_or_mailbox": "Eine Mailbox, ein Alias oder eine sich aus einer Alias-Domain ergebende Adresse mit dem Namen %s ist bereits vorhanden", + "is_alias_or_mailbox": "Eine Mailbox, ein Alias oder eine sich aus einer Alias-Domain ergebende Adresse mit dem Namen %s ist bereits vorhanden.", "is_spam_alias": "%s lautet bereits eine temporäre Alias-Adresse (Spam-Alias-Adresse)", "last_key": "Letzter Key kann nicht gelöscht werden, bitte stattdessen die 2FA deaktivieren.", "login_failed": "Anmeldung fehlgeschlagen", @@ -512,6 +519,7 @@ "pushover_credentials_missing": "Pushover Token und/oder Key fehlen", "pushover_key": "Pushover Key hat das falsche Format", "pushover_token": "Pushover Token hat das falsche Format", + "quarantine_category_invalid": "Quarantäne-Kategorie muss eine der folgenden sein: add_header, reject, all", "quota_not_0_not_numeric": "Speicherplatz muss numerisch und >= 0 sein", "recipient_map_entry_exists": "Eine Empfängerumschreibung für Objekt \"%s\" existiert bereits", "recovery_email_failed": "E-Mail zur Wiederherstellung konnte nicht gesendet werden. Bitte wenden Sie sich an Ihren Administrator.", @@ -536,6 +544,7 @@ "temp_error": "Temporärer Fehler", "text_empty": "Text darf nicht leer sein", "tfa_token_invalid": "TFA-Token ungültig", + "tfa_removal_blocked": "Zwei-Faktor-Authentifizierung kann nicht entfernt werden, da sie für Ihr Konto erforderlich ist.", "tls_policy_map_dest_invalid": "Ziel ist ungültig", "tls_policy_map_entry_exists": "Eine TLS-Richtlinie \"%s\" existiert bereits", "tls_policy_map_parameter_invalid": "Parameter ist ungültig", @@ -546,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", @@ -657,7 +667,7 @@ "delete1": "Lösche Nachricht nach Übertragung vom Quell-Server", "delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind", "delete2duplicates": "Lösche Duplikate im Ziel", - "delete_ays": "Soll der Löschvorgang wirklich ausgeführt werden?", + "delete_ays": "Bitte den Löschvorgang bestätigen.", "description": "Beschreibung", "disable_login": "Login verbieten (Mails werden weiterhin angenommen)", "domain": "Domain bearbeiten", @@ -726,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", @@ -776,7 +787,7 @@ "spam_score": "Einen benutzerdefiniterten Spam-Score festlegen", "subfolder2": "Ziel-Ordner
    (leer = kein Unterordner)", "syncjob": "Sync-Job bearbeiten", - "target_address": "Ziel-Adresse(n)", + "target_address": "Ziel-Adresse(n)(kommasepariert)", "target_domain": "Ziel-Domain", "timeout1": "Timeout für Verbindung zum Remote-Host", "timeout2": "Timeout für Verbindung zum lokalen Host", @@ -958,7 +969,7 @@ "private_comment": "Privater Kommentar", "public_comment": "Öffentlicher Kommentar", "q_add_header": "bei Mail in Junk-Ordner", - "q_all": "bei Reject und Mail in Junk-Ordner", + "q_all": " bei Reject und Mail in Junk-Ordner", "q_reject": "bei Reject", "quarantine_category": "Quarantäne-Benachrichtigungskategorie", "quarantine_notification": "Quarantäne-Benachrichtigung", @@ -1032,7 +1043,8 @@ "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem", "syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit", "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen", - "iam": "Identitätsanbieter" + "iam": "Identitätsanbieter", + "force_tfa": "2-Faktor-Authentifizierung" }, "oauth2": { "access_denied": "Bitte als Mailbox-Nutzer einloggen, um den Zugriff via OAuth2 zu erlauben.", @@ -1084,7 +1096,7 @@ "rspamd_result": "Rspamd-Ergebnis", "sender": "Sender (SMTP)", "sender_header": "Sender (\"From\"-Header)", - "settings_info": "Maximale Anzahl der zurückgehaltenen E-Mails: %s
    Maximale Größe einer zu speichernden E-Mail: %s MiB", + "settings_info": "Maximale Anzahl der zurückgehaltenen E-Mails (pro Mailbox): %s
    Maximale Größe einer zu speichernden E-Mail: %s MiB", "show_item": "Details", "spam": "Spam", "spam_score": "Bewertung", @@ -1201,6 +1213,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", @@ -1226,14 +1239,19 @@ "set_tfa": "Konfiguriere Zwei-Faktor-Authentifizierungsmethode", "start_webauthn_validation": "Starte Validierung", "tfa": "Zwei-Faktor-Authentifizierung", - "tfa_token_invalid": "TFA-Token ungültig!", + "tfa_token_invalid": "2FA-Token ungültig", "totp": "Time-based-OTP (Google Authenticator etc.)", "u2f_deprecated": "Es sieht so aus als wurde der Schlüssel mit der alten U2F Methode registriert. Wir werden die Zwei-Faktor-Authentifizierung deaktivieren und deinen Schlüssel löschen.", "u2f_deprecated_important": "Bitte registriere den Schlüssel im Adminbereich mit der neuen WebAuthn Methode.", "webauthn": "WebAuthn-Authentifizierung", "waiting_usb_auth": "Warte auf USB-Gerät...

    Bitte jetzt den vorgesehenen Taster des USB-Gerätes berühren.", "waiting_usb_register": "Warte auf USB-Gerät...

    Bitte zuerst das obere Passwortfeld ausfüllen und erst dann den vorgesehenen Taster des USB-Gerätes berühren.", - "yubi_otp": "Yubico OTP-Authentifizierung" + "yubi_otp": "Yubico OTP-Authentifizierung", + "force_tfa": "Zwei-Faktor-Authentifizierung beim Login erzwingen", + "force_tfa_info": "Der Benutzer muss Zwei-Faktor-Authentifizierung einrichten, bevor er auf den Bereich zugreifen kann.", + "setup_title": "Zwei-Faktor-Authentifizierung erforderlich", + "setup_required": "Ihr Konto erfordert Zwei-Faktor-Authentifizierung. Bitte richten Sie eine 2FA-Methode ein, um fortzufahren.", + "cancel_setup": "Abbrechen und abmelden" }, "user": { "action": "Aktion", @@ -1312,6 +1330,7 @@ "never": "Niemals", "new_password": "Neues Passwort", "new_password_repeat": "Neues Passwort (Wiederholung)", + "pw_update_required": "Ihr Konto erfordert eine Passwortänderung. Bitte setzen Sie ein neues Passwort, um fortzufahren.", "no_active_filter": "Kein aktiver Filter vorhanden", "no_last_login": "Keine letzte UI-Anmeldung gespeichert", "no_record": "Kein Eintrag", @@ -1349,7 +1368,7 @@ "shared_aliases_desc": "Geteilte Alias-Adressen werden nicht bei benutzerdefinierten Einstellungen, wie die des Spam-Filters oder der Verschlüsselungsrichtlinie, berücksichtigt. Entsprechende Spam-Filter können lediglich von einem Administrator vorgenommen werden.", "show_sieve_filters": "Zeige aktiven Filter des Benutzers", "sogo_profile_reset": "SOGo-Profil zurücksetzen", - "sogo_profile_reset_help": "Das Profil wird inklusive aller Kalender- und Kontaktdaten unwiederbringlich gelöscht.", + "sogo_profile_reset_help": "Das Profil wird inklusive aller Kalender- und Kontaktdaten unwiederbringlich gelöscht.", "sogo_profile_reset_now": "Profil jetzt zurücksetzen", "spam_aliases": "Spam E-Mail-Aliasse", "spam_aliases_info": "Ein Spam-Alias ist eine temporäre E-Mailadresse, die benutzt werden kann, um eine echte E-Mail Adressen zu schützen.
    Optional kann eine Ablaufzeit gesetzt werden, sodass der Alias nach dem definierten Zeitraum automatisch deaktiviert wird, was missbrauchte oder geleakte Adressen effektiv entsorgt.", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 79175e578..1f1c1f273 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -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 data/web/css/themes/<name>/ with the following files:
    • theme.css (required)
    • dark.css (optional, dark mode)
    • bootstrap.css (optional, replaces the Bootstrap base theme)
    • theme.json (optional, name and description)
    ", + "ui_theme_none": "No themes found.", "unban_pending": "unban pending", "unchanged_if_empty": "If unchanged leave blank", "upload": "Upload", @@ -416,6 +422,7 @@ }, "danger": { "access_denied": "Access denied or invalid form data", + "tfa_removal_blocked": "Two-factor authentication cannot be removed because it is required for your account.", "alias_domain_invalid": "Alias domain %s is invalid", "alias_empty": "Alias address must not be empty", "alias_goto_identical": "Alias and goto address must not be identical", @@ -443,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", @@ -512,6 +520,7 @@ "pushover_credentials_missing": "Pushover token and or key missing", "pushover_key": "Pushover key has a wrong format", "pushover_token": "Pushover token has a wrong format", + "quarantine_category_invalid": "Quarantine category must be one of: add_header, reject, all", "quota_not_0_not_numeric": "Quota must be numeric and >= 0", "recipient_map_entry_exists": "A Recipient map entry \"%s\" exists", "recovery_email_failed": "Could not send a recovery email. Please contact your administrator.", @@ -550,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", @@ -726,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", @@ -927,6 +938,7 @@ "filters": "Filters", "fname": "Full name", "force_pw_update": "Force password update at next login", + "force_tfa": "TFA", "gal": "Global Address List", "goto_ham": "Learn as ham", "goto_spam": "Learn as spam", @@ -1084,7 +1096,7 @@ "rspamd_result": "Rspamd result", "sender": "Sender (SMTP)", "sender_header": "Sender (\"From\" header)", - "settings_info": "Maximum amount of elements to be quarantined: %s
    Maximum email size: %s MiB", + "settings_info": "Maximum amount of elements to be quarantined (per mailbox): %s
    Maximum email size: %s MiB", "show_item": "Show item", "spam": "Spam", "spam_score": "Score", @@ -1208,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", @@ -1221,6 +1234,11 @@ "confirm_totp_token": "Please confirm your changes by entering the generated token", "delete_tfa": "Disable TFA", "disable_tfa": "Disable TFA until next successful login", + "force_tfa": "Force 2FA enrollment at login", + "force_tfa_info": "The user will be required to set up two-factor authentication before accessing the panel.", + "setup_title": "Two-Factor Authentication Required", + "setup_required": "Your account requires two-factor authentication. Please set up a 2FA method to continue.", + "cancel_setup": "Cancel and log out", "enter_qr_code": "Your TOTP code if your device cannot scan QR codes", "error_code": "Error code", "init_webauthn": "Initializing, please wait...", @@ -1327,6 +1345,7 @@ "overview": "Overview", "password": "Password", "password_now": "Current password (confirm changes)", + "pw_update_required": "Your account requires a password change. Please set a new password to continue.", "password_repeat": "Password (repeat)", "password_reset_info": "If no email for password recovery is provided, this function cannot be used.", "protocols": "Protocols", diff --git a/data/web/lang/lang.es-es.json b/data/web/lang/lang.es-es.json index 7d86973ac..1fb7becfd 100644 --- a/data/web/lang/lang.es-es.json +++ b/data/web/lang/lang.es-es.json @@ -17,7 +17,7 @@ "spam_policy": "Lista de bloqueo/desbloqueo", "spam_score": "Puntuación de spam", "syncjobs": "Trabajos de sincronización", - "tls_policy": "Póliza de TLS", + "tls_policy": "Política de TLS", "unlimited_quota": "Cuota ilimitada para buzones", "app_passwds": "Gestionar las contraseñas de aplicaciones", "domain_desc": "Cambiar descripción del dominio", @@ -28,7 +28,8 @@ "pw_reset": "Permitir el restablecimiento de la contraseña del usuario mailcow", "sogo_access": "Permitir la gestión del acceso a SOGo", "mailbox_relayhost": "Cambiar el host de reenvío para un buzón", - "smtp_ip_access": "Cambiar hosts permitidos para SMTP" + "smtp_ip_access": "Cambiar hosts permitidos para SMTP", + "alias_external_goto": "Permitir alias con dominios de destino externos" }, "add": { "activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.", @@ -78,8 +79,8 @@ "relay_all": "Retransmitir todos los destinatarios", "relay_all_info": "↪ Si se elige no retransmitir todos los destinatarios, será necesario agregar un buzón \"ciego\" por cada destinatario que deba ser retransmitido.", "relay_domain": "Retransmitir este dominio", - "select": "Por favor selecciona...", - "select_domain": "Por favor elige un dominio primero", + "select": "Seleccione...", + "select_domain": "Elija primero un dominio", "sieve_desc": "Descripción", "sieve_type": "Tipo de filtro", "skipcrossduplicates": "Omitir mensajes duplicados en carpetas (orden de llegada)", @@ -108,7 +109,9 @@ "domain_matches_hostname": "El dominio %s coincide con el nombre de host", "relay_unknown_only": "Reenviar sólo los buzones no existentes. Los buzones existentes se entregarán localmente.", "relayhost_wrapped_tls_info": "Por favor, no utilice puertos con TLS (habitualmente, el puerto 465).
    Utilice cualquier puerto no cifrado y emita STARTTLS. Se puede crear una política para imponer TLS en \"TLS policy maps\".", - "tags": "Etiquetas" + "tags": "Etiquetas", + "internal_info": "Los alias internos solo son accesibles desde el propio dominio o sus dominios alias.", + "sender_allowed": "Permitir enviar como este alias" }, "admin": { "access": "Acceso", @@ -171,7 +174,7 @@ "f2b_max_ban_time": "Max tiempo de restricción (s)", "f2b_netban_ipv4": "Tamaño de subred IPv4 para aplicar la restricción (8-32)", "f2b_netban_ipv6": "Tamaño de subred IPv6 para aplicar la restricción (8-128)", - "f2b_parameters": "Parametros Fail2ban", + "f2b_parameters": "Parámetros de Fail2ban", "f2b_retry_window": "Ventana de tiempo entre reintentos", "f2b_whitelist": "Redes y hosts en lista de desbloqueo", "filter_table": "Filtrar tabla", @@ -190,7 +193,7 @@ "include_exclude_info": "Por defecto - sin selección - todos los buzones son notificados.", "includes": "Incluye a estos destinatarios", "link": "Enlace", - "loading": "Espera por favor...", + "loading": "Espere, por favor...", "logo_info": "La imagen se escalará a una altura de 40 px para la barra de navegación superior y un máx. de ancho de 250 px para la página de inicio. Un gráfico escalable es muy recomendable.", "main_name": "Nombre \"mailcow UI\"", "merged_vars_hint": "Las filas grises fueron incorporadas de vars.(local.)inc.php y no son modificables.", @@ -281,7 +284,7 @@ "password_length": "Longitud de la contraseña", "quicklink_text": "Mostrar u ocultar enlaces rápidos a otras páginas de inicio bajo el formulario de inicio de sesión", "password_policy_lowerupper": "Debe contener caracteres en minúsculas y mayúsculas", - "rspamd_global_filters_regex": "Sus nombres indican su propósito. Todo el contenido debe constar de expresiones regulares válidas con el formato \"/patrón/opciones\" (por ejemplo, /.+@domain\\.tld/i).
    \n Si bien se llevan a cabo comprobaciones básicas de cada expresión regular, la funcionalidad de Rspamd puede verse inutilizada, si no consigue interpretar correctamente la sintaxis utilizada.
    \n Rspamd intentará leer el contenido del mapa cuando éste se modifique. En caso de de problemas, reinicie Rspamd para forzar una recarga del mapa.
    Los elementos incluidos en listas de bloqueo se excluyen de la cuarentena.", + "rspamd_global_filters_regex": "Sus nombres indican su propósito. Todo el contenido debe constar de expresiones regulares válidas con el formato \"/patrón/opciones\" (por ejemplo, /.+@domain\\.tld/i).
    \n Si bien se llevan a cabo comprobaciones básicas de cada expresión regular, la funcionalidad de Rspamd puede verse inutilizada, si no consigue interpretar correctamente la sintaxis utilizada.
    \n Rspamd intentará leer el contenido del mapa cuando este se modifique. En caso de problemas, reinicie Rspamd para forzar una recarga del mapa.
    Los elementos incluidos en listas de bloqueo se excluyen de la cuarentena.", "iam_use_ssl_info": "Si se habilita SSL y el puerto se establece en el 389, se cambiará automáticamente al 636.", "iam_login_provisioning": "Crear usuarios automáticamente al iniciar sesión", "iam_periodic_full_sync": "Sincronización completa periódica", @@ -400,7 +403,20 @@ "user_quicklink": "Ocultar enlace rápido a página de inicio de sesión de usuario", "validate_license_now": "Validar el GUID contra el servidor de licencias", "verify": "Verificar", - "yes": "✓" + "yes": "✓", + "iam_auth_flow_info": "Además del Authorization Code Flow (Standard Flow en Keycloak), que se usa para el inicio de sesión único (SSO), mailcow también admite autenticación con credenciales directas. El Mailpassword Flow intenta validar las credenciales del usuario mediante la API REST de administración de Keycloak. mailcow obtiene la contraseña cifrada del atributo mailcow_password, que está mapeado en Keycloak.", + "iam_extra_permission": "Para que los siguientes ajustes funcionen, el cliente de mailcow en Keycloak necesita una Service account y el permiso view-users.", + "iam_bindpass": "Contraseña de enlace (bind)", + "iam_rest_flow": "Mailpassword Flow", + "iam_binddn": "Bind DN", + "oauth2_info": "La implementación de OAuth2 admite el tipo de concesión \"Authorization Code\" y emite tokens de actualización.
    \r\nEl servidor también emite automáticamente nuevos tokens de actualización después de usar uno.

    \r\n• El alcance predeterminado es profile. Solo los usuarios de buzones pueden autenticarse mediante OAuth2. Si se omite el parámetro scope, se usa profile.
    \r\n• El cliente debe enviar el parámetro state como parte de la solicitud de autorización.

    \r\nRutas para las solicitudes a la API de OAuth2:
    \r\n
      \r\n
    • Endpoint de autorización: /oauth/authorize
    • \r\n
    • Endpoint de tokens: /oauth/token
    • \r\n
    • Página de recursos: /oauth/profile
    • \r\n
    \r\nRegenerar el secreto del cliente no invalida los códigos de autorización existentes, pero estos no podrán renovar su token.

    \r\nRevocar los tokens de los clientes termina de inmediato todas las sesiones activas. Todos los clientes deberán volver a autenticarse.", + "rate_name": "Nombre del límite", + "ui_header_announcement_active": "Activar anuncio", + "ui_theme": "Tema", + "ui_theme_dark": "Incluye una variante en modo oscuro", + "ui_theme_default": "Predeterminado", + "ui_theme_info": "Los temas son carpetas en data/web/css/themes/<name>/ con los siguientes archivos:
    • theme.css (obligatorio)
    • dark.css (opcional, modo oscuro)
    • bootstrap.css (opcional, reemplaza el tema base de Bootstrap)
    • theme.json (opcional, name y description)
    ", + "ui_theme_none": "No se encontraron temas." }, "danger": { "access_denied": "Acceso denegado o datos del formulario inválidos", @@ -447,7 +463,7 @@ "map_content_empty": "Contenido no puede estar vacío", "max_mailbox_exceeded": "Máx. de buzones superado (%d de %d)", "max_quota_in_use": "Cuota del buzón debe ser mayor o igual a %d MiB", - "mysql_error": "MySQL error: %s", + "mysql_error": "Error de MySQL: %s", "network_host_invalid": "Red o host inválido: %s", "next_hop_interferes": "%s interfiere con el siguiente destino %s", "next_hop_interferes_any": "Siguiente destino existente interfiere con %s", @@ -462,7 +478,7 @@ "private_key_error": "Error en la llave privada: %s", "quota_not_0_not_numeric": "Cuota debe ser numérica y >= 0", "recipient_map_entry_exists": "Una regla de destinatario \"%s\" existe", - "redis_error": "Redis error: %s", + "redis_error": "Error de Redis: %s", "relayhost_invalid": "relayhost %s es invalido", "release_send_failed": "El mensaje no pudo ser liberado: %s", "rl_timeframe": "Marco de tiempo del límite de velocidad esta incorrecto", @@ -471,14 +487,14 @@ "sender_acl_invalid": "Valor %s del ACL del remitente es inválido", "set_acl_failed": "Error al establecer el ACL", "settings_map_invalid": "Ajustes de ID %s invalidos", - "sieve_error": "Sieve parser error: %s", + "sieve_error": "Error del analizador de Sieve: %s", "spam_learn_error": "Error aprendiendo muestra: %s", "subject_empty": "Asunto no puede estar vacío", "target_domain_invalid": "El dominio \"goto\" es inválido", "targetd_not_found": "Dominio destino no encontrado", "text_empty": "Texto no puede estar vacío", - "tls_policy_map_entry_exists": "Regla de póliza de TLS \"%s\" existe", - "tls_policy_map_parameter_invalid": "El parámetro de póliza no es válido.", + "tls_policy_map_entry_exists": "La regla de política de TLS \"%s\" ya existe", + "tls_policy_map_parameter_invalid": "El parámetro de la política no es válido.", "totp_verification_failed": "Verificación TOTP fallida", "transport_dest_exists": "Destino de la regla de transporte ya existe", "webauthn_verification_failed": "Verificación WebAuthn fallida: %s", @@ -541,7 +557,12 @@ "tfa_token_invalid": "Token de autenticación de doble factor no válido", "to_invalid": "El destinatario no puede quedar en blanco", "webauthn_authenticator_failed": "No se ha localizado el autenticador seleccionado", - "webauthn_username_failed": "El autenticador seleccionado pertenece a otra cuenta" + "webauthn_username_failed": "El autenticador seleccionado pertenece a otra cuenta", + "tfa_removal_blocked": "No se puede quitar la autenticación de dos factores porque es obligatoria para su cuenta.", + "external_goto_denied": "La dirección de destino externa %s no está permitida", + "max_age_invalid": "La antigüedad máxima %s no es válida", + "quarantine_category_invalid": "La categoría de cuarentena debe ser una de: add_header, reject, all", + "ui_theme_invalid": "Tema no encontrado" }, "debug": { "containers_info": "Información de los contenedores", @@ -551,11 +572,11 @@ "log_info": "

    Los logs en memoria son recopilados en listas de Redis y recortados a LOG_LINES (%d) cada minuto para prevenir sobrecarga en el sistema.\r\n
    Los logs en memoria no están destinados a ser persistentes. Todas las aplicaciones que logean a la memoria, también logean en el daemon de Docker y, por lo tanto, en el controlador de registro predeterminado.\r\n El log en memoria se debe utilizar para analizar problemas menores con los contenedores.

    \r\n

    Los logs externos se recopilan a través de la API de la aplicación dada.

    \r\n

    Los logs estáticos son principalmente registros de actividad, que no están registrados en Dockerd pero que aún deben ser persistentes (excepto los registros de API).

    ", "logs": "Logs", "restart_container": "Reiniciar", - "docs": "Docs", + "docs": "Documentación", "last_modified": "Última modificación", "size": "Tamaño", "started_at": "Iniciado el", - "uptime": "Uptime", + "uptime": "Tiempo en funcionamiento", "static_logs": "Logs estáticos", "system_containers": "Sistema y Contenedores", "show_ip": "Mostrar IP pública", @@ -617,8 +638,8 @@ "full_name": "Nombre completo", "gal": "Lista global de direcciones (GAL)", "gal_info": "El GAL contiene todos los objetos de un dominio y no puede ser editado por ningún usuario. Falta información de disponibilidad en SOGo, si está desactivada. Reinicia SOGo para aplicar los cambios.", - "grant_types": "Grant types", - "hostname": "Hostname", + "grant_types": "Tipos de concesión", + "hostname": "Nombre de host", "inactive": "Inactivo", "kind": "Tipo", "last_modified": "Última modificación", @@ -636,9 +657,9 @@ "password_repeat": "Confirmación de contraseña (repetir):", "previous": "Página anterior", "quota_mb": "Cuota (MiB)", - "redirect_uri": "Redirect/Callback URL", + "redirect_uri": "URL de redirección/callback", "relay_all": "Retransmitir todos los destinatarios", - "relay_all_info": "Si eliges no retransmitir a todos los destinatarios, necesitas agregar un buzón \"ciego\" por cada destinatario que debe ser retransmitido.", + "relay_all_info": "Si elige no retransmitir a todos los destinatarios, deberá agregar un buzón \"ciego\" por cada destinatario que deba ser retransmitido.", "relay_domain": "Dominio de retransmisión", "relayhost": "Ruta saliente", "remove": "Eliminar", @@ -715,12 +736,36 @@ "spam_alias": "Crear o modificar alias temporales (con caducidad)", "spam_filter": "Filtro de spam", "spam_policy": "Añadir o eliminar elementos de la lista de bloqueo/desbloqueo", - "spam_score": "Establecer una puntuación de spam personalizada" + "spam_score": "Establecer una puntuación de spam personalizada", + "domain_footer_info_vars": { + "auth_user": "{= auth_user =} - Nombre de usuario autenticado indicado por un MTA", + "from_user": "{= from_user =} - Parte de usuario del remitente del sobre, p. ej. para \"moo@mailcow.tld\" devuelve \"moo\"", + "from_name": "{= from_name =} - Nombre del remitente del sobre, p. ej. para \"Mailcow <moo@mailcow.tld>\" devuelve \"Mailcow\"", + "from_addr": "{= from_addr =} - Dirección del remitente del sobre", + "from_domain": "{= from_domain =} - Dominio del remitente del sobre", + "custom": "{= foo =} - Si el buzón tiene el atributo personalizado \"foo\" con el valor \"bar\", devuelve \"bar\"" + }, + "domain_footer_plain": "Pie de página en texto plano", + "internal_info": "Los alias internos solo son accesibles desde el propio dominio o sus dominios alias.", + "sender_allowed": "Permitir enviar como este alias", + "sender_allowed_info": "Si se desactiva, este alias solo puede recibir correo. Use la ACL de remitentes para anularlo y dar permiso de envío a buzones concretos.", + "mta_sts": "MTA-STS", + "mta_sts_info": "MTA-STS es un estándar que obliga a que la entrega de correo entre servidores use TLS con certificados válidos.
    Se usa cuando DANE no es posible por falta de DNSSEC o porque no es compatible.
    Nota: si el dominio receptor admite DANE con DNSSEC, DANE siempre tiene preferencia; MTA-STS solo actúa como alternativa.", + "mta_sts_version_info": "Define la versión del estándar MTA-STS; actualmente solo es válida STSv1.", + "mta_sts_mode_info": "Hay tres modos para elegir:
    • testing: la política solo se supervisa, las infracciones no tienen efecto.
    • enforce: la política se aplica estrictamente y se rechazan las conexiones sin TLS válido.
    • none: la política se publica pero no se aplica.
    ", + "mta_sts_max_age": "Antigüedad máxima", + "mta_sts_max_age_info": "Tiempo en segundos durante el cual los servidores receptores pueden guardar esta política en caché antes de volver a obtenerla.", + "mta_sts_mx_info": "Permite enviar solo a los nombres de servidor de correo indicados explícitamente; el MTA emisor comprueba que el nombre del registro MX coincida con la lista de la política y solo entrega con un certificado TLS válido (protege contra ataques MITM).", + "mta_sts_mx_notice": "Se pueden indicar varios servidores MX (separados por comas).", + "mta_sts_active_info": "Si se desmarca, la política MTA-STS no se publicará y no se solicitará ningún certificado para el subdominio mta-sts mediante ACME.", + "pushover_only_x_prio": "Considerar solo el correo de alta prioridad [X-Priority: 1]", + "pushover_sender_regex": "Considerar la siguiente expresión regular de remitente", + "pushover_vars": "Si no se define ningún filtro de remitente, se considerarán todos los correos.
    Los filtros por expresión regular y las comprobaciones exactas de remitente pueden definirse por separado y se evalúan en orden. No dependen entre sí.
    Variables utilizables para el texto y el título (tenga en cuenta las normas de protección de datos)" }, "footer": { - "hibp_nok": "¡Se encontró coincidencia - esta es una contraseña no segura, selecciona otra!", + "hibp_nok": "¡Se encontró una coincidencia: esta es una contraseña no segura, elija otra!", "hibp_ok": "No se encontraron coincidencias", - "loading": "Espera por favor...", + "loading": "Espere, por favor...", "restart_now": "Reiniciar ahora", "restart_container": "Reiniciar contenedor", "restart_container_info": "Importante: Un reinicio limpio puede llevar un tiempo. Por favor, espere a que finalice.", @@ -735,7 +780,7 @@ "header": { "administration": "Administración", "debug": "Información", - "email": "E-Mail", + "email": "Correo electrónico", "mailcow_config": "Configuración", "quarantine": "Cuarentena", "restart_sogo": "Reiniciar SOGo", @@ -781,12 +826,12 @@ "add_bcc_entry": "Añadir regla de BCC", "add_domain": "Agregar dominio", "add_domain_alias": "Agregar alias de dominio", - "add_domain_record_first": "Agrega un dominio primero", + "add_domain_record_first": "Agregue primero un dominio", "add_filter": "Añadir filtro", "add_mailbox": "Agregar buzón", "add_recipient_map_entry": "Añadir regla de destinatario", "add_resource": "Añadir recurso", - "add_tls_policy_map": "Añadir regla de póliza de TLS", + "add_tls_policy_map": "Agregar regla de política de TLS", "address_rewriting": "Reescritura de direcciones", "alias": "Alias", "aliases": "Alias", @@ -864,8 +909,8 @@ "running": "En marcha", "set_postfilter": "Marcar como posfiltro", "set_prefilter": "Marcar como prefiltro", - "sieve_info": "Puedes almacenar múltiples filtros por usuario, pero solo un prefiltro y un postfiltro pueden estar activos al mismo tiempo.
    \r\nCada filtro será procesado en el orden descrito. Ni un script fallido ni un \"keep;\" emitido detendrá el procesamiento de otros scripts.
    \r\nPrefiltro sieve global → Prefiltro → Scripts del usuario → Posfiltro → Posfiltro sieve global", - "status": "Status", + "sieve_info": "Puede almacenar varios filtros por usuario, pero solo un prefiltro y un postfiltro pueden estar activos al mismo tiempo.
    \r\nCada filtro será procesado en el orden descrito. Ni un script fallido ni un \"keep;\" emitido detendrá el procesamiento de otros scripts.
    \r\nPrefiltro sieve global → Prefiltro → Scripts del usuario → Posfiltro → Posfiltro sieve global", + "status": "Estado", "sync_jobs": "Trabajos de sincronización", "target_address": "Dirección destino", "target_domain": "Dominio destino", @@ -873,12 +918,12 @@ "tls_enforce_out": "Forzar TLS saliente", "tls_map_dest": "Destino", "tls_map_dest_info": "Ejemplos: example.org, .example.org, [mail.example.org]:25", - "tls_map_parameters": "Parametros", - "tls_map_parameters_info": "Vacío o parametros, por ejemplo: protocols=!SSLv2 ciphers=medium exclude=3DES", - "tls_map_policy": "Póliza", + "tls_map_parameters": "Parámetros", + "tls_map_parameters_info": "Vacío o parámetros, por ejemplo: protocols=!SSLv2 ciphers=medium exclude=3DES", + "tls_map_policy": "Política", "tls_policy_maps": "Políticas de TLS", - "tls_policy_maps_info": "Esta póliza anula las reglas de transporte TLS salientes independientemente de la configuración de la política TLS de los usuarios.
    \r\n Revisa the \"smtp_tls_policy_maps\" docs para mas información.", - "tls_policy_maps_long": "Forzar póliza de TLS saliente", + "tls_policy_maps_info": "Este mapa de políticas anula las reglas de transporte TLS salientes, independientemente de la configuración de la política TLS de los usuarios.
    \r\n Consulte la documentación de \"smtp_tls_policy_maps\" para más información.", + "tls_policy_maps_long": "Forzar política de TLS saliente", "toggle_all": "Selecionar todo", "username": "Nombre de usuario", "waiting": "Esperando", @@ -942,7 +987,12 @@ "table_size_show_n": "Mostrar %s elementos", "templates": "Plantillas", "template": "Plantilla", - "yes": "✓" + "yes": "✓", + "add_alias_expand": "Extender el alias a los dominios alias", + "alias_domain_backupmx": "Dominio alias inactivo para el dominio de retransmisión", + "catch_all": "Recoge-todo (catch-all)", + "force_tfa": "2FA", + "last_run_reset": "Programar la siguiente" }, "oauth2": { "access_denied": "Inicie sesión como propietario del buzón para otorgar acceso a través de OAuth2.", @@ -967,8 +1017,8 @@ "medium_danger": "Peligro medio", "neutral_danger": "Neutral", "qhandler_success": "Solicitud enviada con éxito al sistema, puede cerrar la ventana", - "qid": "Rspamd QID", - "qitem": "Quarantine item", + "qid": "QID de Rspamd", + "qitem": "Elemento en cuarentena", "quarantine": "Cuarentena", "quick_actions": "Acciones", "rcpt": "Destinatario", @@ -990,14 +1040,29 @@ "download_eml": "Descargar (.eml)", "info": "Información", "junk_folder": "Carpeta de correo no deseado", - "notified": "Notificado" + "notified": "Notificado", + "qinfo": "El sistema de cuarentena guarda en la base de datos el correo rechazado (al remitente no se le dará la impresión de que el correo fue entregado) y también el correo que se entrega como copia en la carpeta de correo no deseado de un buzón.\r\n
    \"Aprender como spam y eliminar\" aprende un mensaje como spam mediante el teorema de Bayes y calcula hashes difusos para rechazar mensajes similares en el futuro.\r\n
    Tenga en cuenta que aprender varios mensajes puede llevar tiempo, según su sistema.
    Los elementos de la lista de bloqueo se excluyen de la cuarentena.", + "quick_delete_link": "Abrir enlace de eliminación rápida", + "quick_release_link": "Abrir enlace de liberación rápida", + "rewrite_subject": "Reescribir asunto", + "sender_header": "Remitente (encabezado \"From\")", + "settings_info": "Cantidad máxima de elementos en cuarentena (por buzón): %s
    Tamaño máximo de correo: %s MiB" }, "queue": { - "queue_manager": "Administrador de cola" + "queue_manager": "Administrador de cola", + "flush": "Vaciar la cola", + "info": "La cola de correo contiene todos los correos que esperan ser entregados. Si un correo permanece mucho tiempo en la cola, el sistema lo elimina automáticamente.
    El mensaje de error de cada correo indica por qué no se pudo entregar.", + "legend": "Acciones de la cola de correo:", + "deliver_mail_legend": "Intenta volver a entregar los correos seleccionados.", + "hold_mail_legend": "Retiene los correos seleccionados. (Impide nuevos intentos de entrega)", + "show_message": "Mostrar mensaje", + "unban": "desbloquear cola", + "unhold_mail": "Liberar", + "unhold_mail_legend": "Libera los correos seleccionados para su entrega. (Requiere que hayan sido retenidos antes)" }, "start": { "help": "Mostrar/Ocultar panel de ayuda", - "imap_smtp_server_auth_info": "Por favor utiliza tu dirección de correo completa y el mecanismo de autenticación PLAIN.
    \r\nTus datos para iniciar sesión serán cifrados por el cifrado obligatorio del servidor" + "imap_smtp_server_auth_info": "Utilice su dirección de correo completa y el mecanismo de autenticación PLAIN.
    \r\nSus datos de inicio de sesión se cifran gracias al cifrado obligatorio del servidor" }, "success": { "acl_saved": "ACL para el objeto %s guardado", @@ -1024,8 +1089,8 @@ "domain_admin_removed": "Administrador del dominio %s removido", "domain_modified": "Cambios al dominio %s guardados", "domain_removed": "Dominio %s removido", - "forwarding_host_added": "Forwarding host %s has been added", - "forwarding_host_removed": "Forwarding host %s has been removed", + "forwarding_host_added": "Se agregó el host de reenvío %s", + "forwarding_host_removed": "Se eliminó el host de reenvío %s", "logged_in_as": "Conectado como %s", "mailbox_added": "Buzón %s agregado", "mailbox_modified": "Cambios al buzón %s guardados", @@ -1043,31 +1108,86 @@ "saved_settings": "Ajustes guardados", "settings_map_added": "Ajustes agregados", "settings_map_removed": "Ajustes del ID %s removidos", - "tls_policy_map_entry_deleted": "Regla de póliza de TLS con ID %s ha sido elimindada", - "tls_policy_map_entry_saved": "Regla de póliza de TLS \"%s\" ha sido guardada", + "tls_policy_map_entry_deleted": "Se eliminó la regla de política de TLS con ID %s", + "tls_policy_map_entry_saved": "Se guardó la regla de política de TLS \"%s\"", "verified_totp_login": "Inicio de sesión TOTP verificado", "verified_webauthn_login": "Inicio de sesión WebAuthn verificado", - "verified_yotp_login": "Inicio de sesión Yubico OTP verificado" + "verified_yotp_login": "Inicio de sesión Yubico OTP verificado", + "admin_added": "Se agregó el administrador %s", + "admin_api_modified": "Se guardaron los cambios en la API", + "admin_removed": "Se eliminó el administrador %s", + "app_links": "Se guardaron los cambios en los enlaces de aplicaciones", + "app_passwd_added": "Se agregó una nueva contraseña de aplicación", + "app_passwd_removed": "Se eliminó la contraseña de aplicación con ID %s", + "cors_headers_edited": "Se guardó la configuración de CORS", + "custom_login_modified": "La personalización del inicio de sesión se guardó correctamente", + "domain_add_dkim_available": "Ya existía una clave DKIM", + "dkim_duplicated": "La clave DKIM del dominio %s se copió a %s", + "domain_footer_modified": "Se guardaron los cambios en el pie de página del dominio %s", + "dovecot_restart_success": "Dovecot se reinició correctamente", + "eas_reset": "Se restablecieron los dispositivos ActiveSync del usuario %s", + "f2b_banlist_refreshed": "El ID de la lista de bloqueo se actualizó correctamente.", + "f2b_modified": "Se guardaron los cambios en los parámetros de Fail2ban", + "global_filter_written": "El filtro se escribió correctamente en el archivo", + "hash_deleted": "Hash eliminado", + "iam_test_connection": "Conexión correcta", + "ip_check_opt_in_modified": "La comprobación de IP se guardó correctamente", + "item_deleted": "Elemento %s eliminado correctamente", + "item_released": "Elemento %s liberado", + "items_deleted": "Elemento %s eliminado correctamente", + "items_released": "Se liberaron los elementos seleccionados", + "learned_ham": "El ID %s se aprendió correctamente como correo legítimo (ham)", + "license_modified": "Se guardaron los cambios en la licencia", + "mailbox_renamed": "El buzón se renombró de %s a %s", + "nginx_reloaded": "Nginx se recargó", + "object_modified": "Se guardaron los cambios en el objeto %s", + "password_policy_saved": "La política de contraseñas se guardó correctamente", + "password_changed_success": "La contraseña se cambió correctamente", + "pushover_settings_edited": "La configuración de Pushover se guardó; verifique las credenciales.", + "recovery_email_sent": "Se envió el correo de recuperación a %s", + "reset_main_logo": "Se restableció el logo predeterminado", + "resource_removed": "Se eliminó el recurso %s", + "sogo_profile_reset": "Se restableció el perfil de SOGo del usuario %s", + "template_added": "Se agregó la plantilla %s", + "template_modified": "Se guardaron los cambios en la plantilla %s", + "template_removed": "Se eliminó la plantilla con ID %s", + "ui_texts": "Se guardaron los cambios en los textos de la interfaz", + "ui_theme_modified": "El tema se guardó correctamente", + "upload_success": "Archivo subido correctamente", + "verified_fido2_login": "Inicio de sesión FIDO2 verificado" }, "tfa": { - "api_register": "%s utiliza la API de la nube de Yubico. Por favor, obtén una clave API para tu llave aquí.", + "api_register": "%s utiliza la API en la nube de Yubico. Obtenga una clave de API para su llave aquí.", "confirm": "Confirmar", - "confirm_totp_token": "Por favor confirma los cambios ingresando el token generado", + "confirm_totp_token": "Confirme los cambios ingresando el token generado", "delete_tfa": "Desactivar TFA", "disable_tfa": "Desactivar TFA hasta el próximo inicio de sesión exitoso", "enter_qr_code": "Su código TOTP si su dispositivo no puede escanear códigos QR", - "key_id": "Un identificador para tu YubiKey", - "key_id_totp": "Un identificador para tu llave", + "key_id": "Un identificador para su YubiKey", + "key_id_totp": "Un identificador para su llave", "none": "Desactivar", "scan_qr_code": "Escanee el siguiente código con su aplicación de autenticador o ingrese el código manualmente.", - "select": "Selecciona", + "select": "Seleccione", "set_tfa": "Establecer el método de autenticación de dos factores", "tfa": "Autenticación de dos factores", "totp": "OTP basado en tiempo (Google Authenticator, Authy, etc.)", "webauthn": "Autenticación WebAuthn", "waiting_usb_auth": "Esperando al dispositivo USB...

    Toque el botón en su dispositivo USB WebAuthn ahora.", "waiting_usb_register": "Esperando al dispositivo USB....

    Ingrese su contraseña arriba y confirme su registro WebAuthn tocando el botón en su dispositivo USB WebAuthn.", - "yubi_otp": "Yubico OTP" + "yubi_otp": "Yubico OTP", + "authenticators": "Autenticadores", + "force_tfa": "Exigir configurar 2FA al iniciar sesión", + "force_tfa_info": "El usuario deberá configurar la autenticación de dos factores antes de acceder al panel.", + "setup_title": "Autenticación de dos factores obligatoria", + "setup_required": "Su cuenta requiere autenticación de dos factores. Configure un método de 2FA para continuar.", + "cancel_setup": "Cancelar y cerrar sesión", + "error_code": "Código de error", + "init_webauthn": "Inicializando, espere...", + "reload_retry": "- (recargue el navegador si el error persiste)", + "start_webauthn_validation": "Iniciar validación", + "tfa_token_invalid": "Token de 2FA no válido", + "u2f_deprecated": "Parece que su llave se registró con el método obsoleto U2F. Desactivaremos la autenticación de dos factores y eliminaremos su llave.", + "u2f_deprecated_important": "Registre su llave en el panel de administración con el nuevo método WebAuthn." }, "user": { "action": "Acción", @@ -1075,7 +1195,7 @@ "alias": "Alias", "alias_create_random": "Generar alias aleatorio", "alias_extend_all": "Extender alias por 1 hora", - "alias_full_date": "d.m.Y, H:i:s T", + "alias_full_date": "d/m/Y, H:i:s T", "alias_remove_all": "Eliminar todos los alias", "alias_select_validity": "Periodo de validez", "alias_time_left": "Tiempo restante", @@ -1126,7 +1246,7 @@ "spamfilter_bl": "Lista negra", "spamfilter_bl_desc": "Direcciones en la lista negra siempre clasificarán como spam. Se puede usar comodines. Filtro solo aplica a los alias directos (alias con un solo buzón de destino) excluyendo los alias catch-all.", "spamfilter_default_score": "Valores predeterminados:", - "spamfilter_green": "Verde: éste mensaje no es spam", + "spamfilter_green": "Verde: este mensaje no es spam", "spamfilter_hint": "El primer valor representa la \"calificación baja de spam\", el segundo representa la \"calificación alta de spam\".", "spamfilter_red": "Rojo: Este mensaje es spam, será rechazado por el servidor y enviado a la quarantena (si esta configurada)", "spamfilter_table_action": "Acción", @@ -1136,8 +1256,8 @@ "spamfilter_table_rule": "Regla", "spamfilter_wl": "Lista blanca", "spamfilter_wl_desc": "Direcciones en la lista blanca nunca clasificarán como spam. Se puede usar comodines. Filtro solo aplica a los alias directos (alias con un solo buzón de destino) excluyendo los alias catch-all.", - "spamfilter_yellow": "Amarillo: éste mensaje puede ser spam, será etiquetado como spam y trasladado a tu carpeta de correo no deseado", - "status": "Status", + "spamfilter_yellow": "Amarillo: este mensaje puede ser spam, será etiquetado como spam y movido a su carpeta de correo no deseado", + "status": "Estado", "sync_jobs": "Trabajos de sincronización", "tag_handling": "Establecer manejo para el correo etiquetado", "tag_help_example": "Ejemplo de una dirección email etiquetada: mi+Facebook@ejemplo.org", @@ -1157,13 +1277,89 @@ "weeks": "Semanas", "with_app_password": "con contraseña de aplicación", "year": "año", - "years": "años" + "years": "años", + "active_sieve": "Filtro activo", + "advanced_settings": "Configuración avanzada", + "app_hint": "Las contraseñas de aplicación son contraseñas alternativas para iniciar sesión en IMAP, SMTP, CalDAV, CardDAV y EAS. El nombre de usuario no cambia. El webmail SOGo no está disponible con contraseñas de aplicación.", + "allowed_protocols": "Protocolos permitidos", + "app_name": "Nombre de la aplicación", + "app_passwds": "Contraseñas de aplicación", + "apple_connection_profile": "Perfil de conexión de Apple", + "apple_connection_profile_complete": "Este perfil de conexión incluye los parámetros de IMAP y SMTP, y las rutas de CalDAV (calendarios) y CardDAV (contactos) para un dispositivo Apple.", + "apple_connection_profile_mailonly": "Este perfil de conexión incluye los parámetros de configuración de IMAP y SMTP para un dispositivo Apple.", + "apple_connection_profile_with_app_password": "Se genera una nueva contraseña de aplicación y se agrega al perfil, para que no haga falta escribir ninguna contraseña al configurar el dispositivo. No comparta el archivo: da acceso completo a su buzón.", + "attribute": "Atributo", + "authentication": "Autenticación", + "change_password_hint_app_passwords": "Su cuenta tiene %d contraseñas de aplicación que no se cambiarán. Para administrarlas, vaya a la pestaña Contraseñas de aplicación.", + "clear_recent_successful_connections": "Borrar las conexiones correctas registradas", + "client_configuration": "Mostrar guías de configuración para clientes de correo y teléfonos", + "create_app_passwd": "Crear contraseña de aplicación", + "delete_ays": "Confirme la eliminación.", + "direct_protocol_access": "Este usuario de buzón tiene acceso directo y externo a los siguientes protocolos y aplicaciones. Su administrador controla este ajuste. Puede crear contraseñas de aplicación para dar acceso a protocolos y aplicaciones concretos.
    El botón \"Webmail\" ofrece inicio de sesión único en SOGo y siempre está disponible.", + "email": "Correo electrónico", + "email_and_dav": "Correo, calendarios y contactos", + "empty": "Sin resultados", + "fido2_webauthn": "FIDO2/WebAuthn", + "force_pw_update": "Debe establecer una nueva contraseña para acceder a los servicios de groupware.", + "from": "de", + "generate": "generar", + "in_use": "En uso", + "last_mail_login": "Último inicio de sesión de correo", + "last_pw_change": "Último cambio de contraseña", + "last_ui_login": "Último inicio de sesión en la interfaz", + "loading": "Cargando...", + "login_history": "Historial de inicios de sesión", + "mailbox": "Buzón", + "mailbox_general": "General", + "mailbox_settings": "Configuración", + "messages": "mensajes", + "month": "mes", + "months": "meses", + "no_active_filter": "No hay ningún filtro activo", + "no_last_login": "No hay información del último inicio de sesión en la interfaz", + "open_logs": "Abrir registros", + "open_webmail_sso": "Webmail", + "overview": "Resumen", + "password": "Contraseña", + "pw_update_required": "Su cuenta requiere un cambio de contraseña. Establezca una nueva contraseña para continuar.", + "password_repeat": "Contraseña (repetir)", + "password_reset_info": "Si no se indica un correo de recuperación de contraseña, esta función no se puede usar.", + "protocols": "Protocolos", + "pushover_evaluate_x_prio": "Escalar el correo de alta prioridad [X-Priority: 1]", + "pushover_info": "La configuración de notificaciones push se aplicará a todo el correo limpio (no spam) entregado a %s, incluidos los alias (compartidos, no compartidos, etiquetados).", + "pushover_only_x_prio": "Considerar solo el correo de alta prioridad [X-Priority: 1]", + "pushover_sender_array": "Considerar las siguientes direcciones de remitente (separadas por comas)", + "pushover_sender_regex": "Filtrar remitentes con la siguiente expresión regular", + "pushover_text": "Texto de la notificación", + "pushover_title": "Título de la notificación", + "pushover_sound": "Sonido", + "pushover_vars": "Si no se define ningún filtro de remitente, se considerarán todos los correos.
    Los filtros por expresión regular y las comprobaciones exactas de remitente pueden definirse por separado y se evalúan en orden. No dependen entre sí.
    Variables utilizables para el texto y el título (tenga en cuenta las normas de protección de datos)", + "pushover_verify": "Verificar credenciales", + "pw_recovery_email": "Correo de recuperación de contraseña", + "q_add_header": "Carpeta de correo no deseado", + "quarantine_category_info": "La categoría de notificación \"Rechazado\" incluye el correo que fue rechazado, mientras que \"Carpeta de correo no deseado\" avisa al usuario de los correos que se movieron a esa carpeta.", + "recent_successful_connections": "Conexiones correctas registradas", + "sender_acl_disabled": "La comprobación de remitente está desactivada", + "show_sieve_filters": "Mostrar el filtro Sieve activo del usuario", + "sogo_profile_reset_help": "Esto destruirá el perfil de SOGo del usuario y eliminará de forma irrecuperable todos sus contactos y datos de calendario.", + "spam_score_reset": "Restablecer al valor predeterminado del servidor", + "spamfilter_table_domain_policy": "n/d (política del dominio)", + "syncjob_check_log": "Revisar registro", + "syncjob_EX_OK": "Correcto", + "syncjob_EXIT_OVERQUOTA": "El buzón de destino superó su cuota", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Usuario o contraseña incorrectos", + "tfa_info": "La autenticación de dos factores ayuda a proteger su cuenta. Si la activa, necesitará contraseñas de aplicación para iniciar sesión en aplicaciones o servicios que no admiten la autenticación de dos factores (p. ej., clientes de correo)." }, "warning": { - "domain_added_sogo_failed": "Se agregó el dominio pero no se pudo reiniciar SOGo, revisa los logs del servidor.", + "domain_added_sogo_failed": "Se agregó el dominio, pero no se pudo reiniciar SOGo; revise los registros del servidor.", "fuzzy_learn_error": "Error aprendiendo hash: %s", "ip_invalid": "IP inválida omitida: %s", - "cannot_delete_self": "No se puede eliminar el usuario conectado" + "cannot_delete_self": "No se puede eliminar el usuario conectado", + "dovecot_restart_failed": "No se pudo reiniciar Dovecot, revise los registros", + "is_not_primary_alias": "Se omitió el alias no principal %s", + "quota_exceeded_scope": "Se superó la cuota del dominio: en este dominio solo se pueden crear buzones ilimitados.", + "session_token": "Token de formulario no válido: los tokens no coinciden", + "session_ua": "Token de formulario no válido: error de validación del User-Agent" }, "datatables": { "collapse_all": "Contraer todo", @@ -1204,5 +1400,12 @@ "register_status": "Estado de registro", "rename": "Renombrar", "start_fido2_validation": "Iniciar validación FIDO2" + }, + "ratelimit": { + "disabled": "Desactivado", + "second": "mensajes / segundo", + "minute": "mensajes / minuto", + "hour": "mensajes / hora", + "day": "mensajes / día" } } diff --git a/data/web/lang/lang.fr-fr.json b/data/web/lang/lang.fr-fr.json index 43c9c065a..9ee916713 100644 --- a/data/web/lang/lang.fr-fr.json +++ b/data/web/lang/lang.fr-fr.json @@ -111,7 +111,8 @@ "app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application", "dry": "Simuler la synchronisation", "internal": "Interne", - "internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine." + "internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine.", + "sender_allowed": "Autoriser l'envoi sous cet alias" }, "admin": { "access": "Accès", @@ -556,7 +557,9 @@ "max_age_invalid": "L'âge maximum %s est invalide", "mode_invalid": "Le mode %s est invalide", "mx_invalid": "L'enregistrement MX %s est invalide", - "version_invalid": "La version %s est invalide" + "version_invalid": "La version %s est invalide", + "tfa_removal_blocked": "L’authentification à deux facteurs ne peut pas être supprimée, car elle est requise pour votre compte.", + "quarantine_category_invalid": "La catégorie de quarantaine doit être l’une des suivantes : add_header, reject, all\"" }, "debug": { "chart_this_server": "Graphique (ce serveur)", @@ -755,7 +758,9 @@ "mta_sts_info": "MTA-STS est un standard qui oblige la délivrance des courriels entre les serveurs de courriels à utiliser TLS avec des certificats valides.
    Il est utilisé quand DANE n'est pas possible à cause d'un manque ou d'un non support de DNSSEC.
    Note : Si le domaine du destinataire supporte DANE avec DNSSEC, DANE est toujours préféré – MTA-STS sert seulement en secours.", "mta_sts_mode_info": "Il y a trois modes parmi lesquels choisir :
    • testing – la politique est seulement surveillée, les violations n'ont pas d'impact.
    • enforce – la politique est appliquée strictement, les connexions sans TLS valide sont rejetées.
    • none – la politique est publiée mais non appliquée.
    ", "mta_sts_max_age_info": "Durée en secondes pendant laquelle les serveurs de courriel peuvent mettre en cache cette politique avant de revérifier.", - "mta_sts_mx_info": "Autoriser l'envoi uniquement aux noms d'hôtes des serveurs de courriels indiqués explicitement ; le MTA émetteur vérifie si le nom d'hôte DNS du MX correspond à la liste de la politique, et autorise la délivrance seulement avec un certificat TLS valide (protège contre le MITM)." + "mta_sts_mx_info": "Autoriser l'envoi uniquement aux noms d'hôtes des serveurs de courriels indiqués explicitement ; le MTA émetteur vérifie si le nom d'hôte DNS du MX correspond à la liste de la politique, et autorise la délivrance seulement avec un certificat TLS valide (protège contre le MITM).", + "sender_allowed": "Autoriser l'envoi sous cet alias", + "sender_allowed_info": "Si cette option est désactivée, cet alias peut uniquement recevoir des courriels. Utilisez les ACL d’expéditeur pour autoriser certaines boîtes aux lettres à envoyer des messages via cet alias." }, "footer": { "cancel": "Annuler", @@ -988,7 +993,8 @@ "recipient": "Destinataire", "open_logs": "Afficher les journaux", "iam": "Fournisseur d'identité", - "internal": "Interne" + "internal": "Interne", + "force_tfa": "TFA" }, "oauth2": { "access_denied": "Veuillez vous connecter en tant que propriétaire de la boîte de réception pour accorder l’accès via Oauth2.", @@ -1189,7 +1195,11 @@ "yubi_otp": "Authentification OTP Yubico", "authenticators": "Authentificateurs", "u2f_deprecated_important": "Veuillez enregistrer votre clé dans le panneau d'administration avec la nouvelle méthode WebAuthn.", - "u2f_deprecated": "Il semble que votre clé ait été enregistrée à l'aide de la méthode U2F obsolète. Nous allons désactiver l'authentification à deux facteurs pour vous et supprimer votre clé." + "u2f_deprecated": "Il semble que votre clé ait été enregistrée à l'aide de la méthode U2F obsolète. Nous allons désactiver l'authentification à deux facteurs pour vous et supprimer votre clé.", + "force_tfa": "Imposer l'authentification à deux facteurs lors de la connexion", + "force_tfa_info": "L’utilisateur doit configurer l’authentification à deux facteurs avant de pouvoir accéder à l’interface.", + "setup_title": "Authentification à deux facteurs requise", + "setup_required": "Votre compte nécessite l’authentification à deux facteurs. Veuillez configurer une méthode 2FA pour continuer." }, "fido2": { "set_fn": "Définir un nom", @@ -1378,7 +1388,8 @@ "forever": "Pour toujours", "spam_aliases_info": "Un alias de spam est une adresse de courriel temporaire qui peut être utilisée pour protéger les véritables adresses de courriel.
    De manière optionnelle, une durée d'expiration peut être définie afin que l'alias soit automatiquement désactivé après la période définie, éliminant ainsi les adresses étant abusées ou ayant fuité.", "authentication": "Authentification", - "protocols": "Protocoles" + "protocols": "Protocoles", + "pw_update_required": "Votre compte nécessite un changement de mot de passe. Veuillez définir un nouveau mot de passe pour continuer." }, "warning": { "cannot_delete_self": "Impossible de supprimer l’utilisateur connecté", diff --git a/data/web/lang/lang.hu-hu.json b/data/web/lang/lang.hu-hu.json index e51748c48..ff49bc5ee 100644 --- a/data/web/lang/lang.hu-hu.json +++ b/data/web/lang/lang.hu-hu.json @@ -1144,7 +1144,8 @@ "subscribeall": "Feliratkozás minden mappára", "syncjob": "Szinkronizálási feladat hozzáadása", "internal": "Belső", - "internal_info": "Belső álnevek csak a saját domain vagy domain álnév számára elérhető." + "internal_info": "Belső álnevek csak a saját domain vagy domain álnév számára elérhető.", + "sender_allowed": "Küldés engedélyezése ezzel az aliasszal" }, "danger": { "access_denied": "Hozzáférés megtagatva vagy nem megfelelő űrlap adat", @@ -1245,6 +1246,21 @@ "pushover_key": "A pushover kulcs rossz formátumú", "pushover_token": "A Pushover token rossz formátumú", "quota_not_0_not_numeric": "A kvótának numerikusnak és >= 0-nak kell lennie.", - "recipient_map_entry_exists": "Létezik egy \"%s\" címzett-térkép bejegyzés" + "recipient_map_entry_exists": "Létezik egy \"%s\" címzett-térkép bejegyzés", + "redis_error": "Redis hiba lépett fel: %s", + "relayhost_invalid": "A(z) %s elem érvénytelen a leképezésben.", + "release_send_failed": "Az üzenet felszabadítása sikertelen: %s", + "reset_f2b_regex": "A regex-szűrő időtúllépés miatt nem állt le. Próbálja újra, vagy várjon egy kicsit, és töltse újra az oldalt.", + "resource_invalid": "A(z) %s erőforrásnév érvénytelen", + "rl_timeframe": "Érvénytelen időkeret a lekérdezési korláthoz", + "rspamd_ui_pw_length": "A Rspamd UI jelszónak legalább 6 karakter hosszúnak kell lennie.", + "script_empty": "A szkript nem lehet üres", + "sender_acl_invalid": "A küldőhöz tartozó ACL-érték (%s) érvénytelen", + "set_acl_failed": "Az ACL beállítása meghiúsult", + "settings_map_invalid": "Érvénytelen beállítás-leképezési azonosító: %s", + "recovery_email_failed": "A helyreállítási email kiküldése sikertelen. Kérlek, lépj kapcsolatba az adminisztrátorral!", + "reset_token_limit_exceeded": "Túl sok visszaállítási kísérlet. Kérjük, várjon, mielőtt újra próbálkozna.", + "required_data_missing": "Hiányzik a(z) szükséges %s adat", + "tfa_removal_blocked": "A kétfaktoros hitelesítés nem távolítható el, mert elengedhetetlen a fiókod használatához." } } diff --git a/data/web/lang/lang.it-it.json b/data/web/lang/lang.it-it.json index a2f0a87e3..de228b2d7 100644 --- a/data/web/lang/lang.it-it.json +++ b/data/web/lang/lang.it-it.json @@ -109,7 +109,10 @@ "bcc_dest_format": "Il destinatario in copia nascosta deve essere un singolo indirizzo email.
    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", diff --git a/data/web/lang/lang.lv-lv.json b/data/web/lang/lang.lv-lv.json index 86eb91a66..df1697282 100644 --- a/data/web/lang/lang.lv-lv.json +++ b/data/web/lang/lang.lv-lv.json @@ -13,7 +13,7 @@ "domain_relayhost": "Mainīt domēna relayhost", "eas_reset": "EAS ierīču atiestatīšana", "extend_sender_acl": "Ļauj paplašināt sūtītāja ACL ar ārējām adresēm", - "login_as": "Pieteikšanās kā pastkastes lietotājam", + "login_as": "Pieteikties kā pastkastes lietotājam", "mailbox_relayhost": "Pasta kastītes relayhost maiņa", "prohibited": "Aizliegts ar ACL", "protocol_access": "Protokola piekļuves maiņa", @@ -38,7 +38,7 @@ "alias_domain": "Aizstājdomēni", "alias_domain_info": "Tikai derīgi domēna vārdi (komatu atdalīti).", "automap": "Mēģiniet automatizēt mapes (\"Nosūtītie vienumi\", \"Nosūtītie\" => \"Nosūtītie\" etc.)", - "backup_mx_options": "Dublējuma MX iespējas", + "backup_mx_options": "Releja iespējas", "delete1": "Izdzēst no avota pēc pabeigšanas", "delete2": "Dzēsiet ziņojumus galamērķī, kas nav avotā", "delete2duplicates": "Izdzēst atkārtojošos vienumus galamērķī", @@ -65,8 +65,8 @@ "relay_all": "Pārsūtīt visus saņēmējus", "relay_all_info": "↪ Ja izvēlies nepārsūtīt visus saņēmējus, tad Tev būs nepieciešams pievienot (\"aklo\") pastkasti katram saņēmējam, kas būtu jāpārsūta.", "relay_domain": "Pārsūtīt šo domēnu", - "select": "Lūdzu izvēlaties...", - "select_domain": "Lūdzu sākumā izvēlaties domēnu", + "select": "Lūgums atlasīt…", + "select_domain": "Lūgums vispirms atlasīt domēnu", "sieve_desc": "Īss apraksts", "sieve_type": "Filtra tips", "skipcrossduplicates": "Izlaist dublētus ziņojumus pa mapēm (pirmais nāk, pirmais kalpo)", @@ -91,12 +91,12 @@ "activate_api": "Aktivizēt API", "active": "Aktīvs", "add": "Pievienot", - "add_domain_admin": "Pievienot domēna administratoru", + "add_domain_admin": "Pievienot domēna pārvaldītāju", "add_forwarding_host": "Pievienot pāradresācijas hostu", "add_relayhost": "Pievienot Relayhost", "add_row": "Pievienot rindu", - "admin": "Administrators", - "admin_details": "Labot administratora detaļas", + "admin": "Pārvaldītājs", + "admin_details": "Labot informāciju par pārvaldītāju", "admin_domains": "Domēna uzdevumi", "api_allow_from": "Atļaut API piekļuvi no šīm IP", "api_key": "API atslēga", @@ -114,7 +114,7 @@ "dkim_keys": "ARC/DKIM atslēgas", "dkim_private_key": "Privāta atslēga", "domain": "Domēns", - "domain_admins": "Domēna administratori", + "domain_admins": "Domēna pārvaldītāji", "edit": "Labot", "empty": "Nav iznākuma", "f2b_ban_time": "Aizlieguma laiks (s)", @@ -153,7 +153,7 @@ "r_info": "Pelēkie/atspējotie vienumi spēkā esošo ierobežojumu sarakstā mailcow nav zināmi kā derīgi ierobežojumi, un tos nevar pārvietot. Nezināmi ierobežojumi jebkurā gadījumā parādīšanas secībā.
    Jaunus vienumus var pievienot inc/vars.local.inc.php, lai varētu tos pārslēgt.", "recipients": "Adresāts", "refresh": "Atsvaidzināt", - "regen_api_key": "Reģenerēt API atslēgu", + "regen_api_key": "Atkārtoti izveidot API atslēgu", "relay_from": "\"No:\" adrese", "relay_run": "Palaist testu", "relayhosts_hint": "Norādīt no sūtītāja atkarīgas piegādes, lai varētu tos atlasīt domēnu konfigurācijas uzvednē.
    \n Piegādes pakalpojums vienmēr ir \"smtp\", tādējādi tiks mēģināts TLS, kad piedāvāts. Iekļautais TLS (SMTPS) netiek atbalstīts. Tiek ņemts vērā lietotāja atsevišķais izejošā TLS nosacījuma iestatījums.
    \n Ietekmē atlasītos domēnus, tajā skaitā aizstājdomēnus.", @@ -185,7 +185,17 @@ "login_time": "Pieteikšanās laiks", "iam_version": "Versija", "quarantine_max_age": "Lielākais pieļaujamais vecums dienās
    Vērtībai jābūt vienādai ar vai lielākai par 1 dienu.", - "quarantine_max_score": "Atmest paziņojumu, ja e-pasta ziņojuma mēstuļu novērtējums ir augstāks par šo vērtību:
    Noklusējums ir 9999.0" + "quarantine_max_score": "Atmest paziņojumu, ja e-pasta ziņojuma mēstuļu novērtējums ir augstāks par šo vērtību:
    Noklusējums ir 9999.0", + "options": "Iespējas", + "password_reset_settings": "Paroļu atkopes iestatījumi", + "password_settings": "Paroļu iestatījumi", + "add_admin": "Pievienot pārvaldītāju", + "admins": "Pārvaldītāji", + "admins_ldap": "LDAP pārvaldītāji", + "admin_quicklink": "Paslēpt ātro saiti uz pārvaldītāju pieteikšanās lapu", + "domain_admin": "Domēna pārvaldītājs", + "domainadmin_quicklink": "Paslēpt ātro saiti uz domēna pārvaldītāju pieteikšanās lapu", + "user_quicklink": "Paslēpt ātro saiti uz lietotāju pieteikšanās lapu" }, "danger": { "access_denied": "Piekļuve liegta, vai nepareizi dati", @@ -241,7 +251,10 @@ "app_passwd_id_invalid": "Lietotnes paroles Id %s ir nederīgs", "img_dimensions_exceeded": "Attēls pārsniedz lielāko pieļaujamo attēla lielumu", "img_size_exceeded": "Attēls pārsniedz lielāko pieļaujamo datnes lielumu", - "version_invalid": "Versija %s ir nederīga" + "version_invalid": "Versija %s ir nederīga", + "generic_server_error": "Atgadījās neparedzēta servera kļūda. Lūgums sazināties ar pārvaldītāju.", + "password_reset_na": "Paroļu atkope šobrīd nav pieejama. Lūgums sazināties ar pārvaldītāju.", + "recovery_email_failed": "Nevarēja nosūtīt atkopes e-pasta ziņojumu. Lūgums sazināties ar pārvaldītāju." }, "diagnostics": { "cname_from_a": "Vērtība, kas iegūta no A/AAAA ieraksta. Tas tiek atbalstīts tik ilgi, kamēr ieraksts norāda uz pareizo resursu.", @@ -257,13 +270,13 @@ "active": "Aktīvs", "alias": "Labot aizstājvārdu", "automap": "Mēģiniet automatizēt mapes (\"Nosūtītie vienumi\", \"Nosūtītie\" => \"Nosūtītie\" utt.)", - "backup_mx_options": "Dublēt MX iespējas", + "backup_mx_options": "Retranslācijas iespējams", "delete1": "Izdzēst no avota pēc pabeigšanas", "delete2": "Dzēsiet ziņojumus galamērķī, kas nav avotā", "delete2duplicates": "Izdzēst atkārtojošos vienumus galamērķī", "description": "Apraksts", "domain": "Labot domēnu", - "domain_admin": "Labot domēna administratoru", + "domain_admin": "Labot domēna pārvaldītāju", "domain_quota": "Domēna kvota", "domains": "Domēni", "dont_check_sender_acl": "Atspējot sūtītāju pārbaudi domēnam %s (+ aizstājdomēni)", @@ -326,7 +339,8 @@ "app_passwd": "Lietotnes parole", "mta_sts_version": "Versija", "mta_sts_version_info": "Norāda MTA-STS standarta versiju – pašreiz ir derīga tikai STSv1.", - "sender_acl_disabled": "Sūtītāja pārbaude ir atspējota" + "sender_acl_disabled": "Sūtītāja pārbaude ir atspējota", + "admin": "Labot pārvaldītāju" }, "footer": { "cancel": "Atcelt", @@ -359,7 +373,14 @@ "username": "Lietotājvārds", "fido2_webauthn": "FIDO/WebAuthn pieteikšanās", "mobileconfig_info": "Lūgums pieteikties kā pastkastes lietotājam, lai lejupielādētu pieprasīto Apple savienojuma profilu.", - "other_logins": "vai pieslēgties ar" + "other_logins": "vai pieteikties ar", + "forgot_password": "> Aizmirsta parole?", + "login_linkstext": "Nepareiza pieteikšanās?", + "login_domainadmintext": "Pieteikties kā domēna pārvaldītājam", + "login_admintext": "Pieteikties kā pārvaldītājam", + "login_user": "Lietotu pieteikšanās", + "login_dadmin": "Domēna pārvaldītāju pieteikšanās", + "login_admin": "Pārvaldītāju pieteikšanās" }, "mailbox": { "action": "Rīcība", @@ -394,7 +415,7 @@ "description": "Apraksts", "dkim_key_length": "DKIM atslēgas garums (bits)", "domain": "Domēns", - "domain_admins": "Domēna administratori", + "domain_admins": "Domēna pārvaldītāji", "domain_aliases": "Domēna aizstājvārdi", "domain_quota": "Kvota", "domain_quota_total": "Kopējais domēna ierobežojums", @@ -504,7 +525,7 @@ "imap_smtp_server_auth_info": "Lūgums izmantot pilnu e-pasta adresi un PLAIN autentificēšanās mehānismu.
    \nPieteikšanās dati tiks šifrēti ar servera puses obligātu šifrēšanu." }, "success": { - "admin_modified": "Izmaiņas administrātoram ir saglabātas", + "admin_modified": "Pārvaldītāja izmaiņas tika saglabātas", "alias_added": "Aizstājadrese %s (%d) tika pievienota", "alias_domain_removed": "Aizstājdomēns %s tika noņemts", "alias_modified": "Aizstājadreses izmaiņas %s tika saglabātas", @@ -515,9 +536,9 @@ "dkim_added": "DKIM atslēga saglabāta", "dkim_removed": "DKIM atslēga %s ir noņemta", "domain_added": "Pievienots domēns %s", - "domain_admin_added": "Domēna administrātors %s pievienots", - "domain_admin_modified": "Izmaiņas domēna administrātoram %s ir saglabātas", - "domain_admin_removed": "Domēna administrators %s tika noņemts", + "domain_admin_added": "Tika pievienots domēna pārvaldītājs %s", + "domain_admin_modified": "Domēna pārvaldītāja %s izmaiņas tika saglabātas", + "domain_admin_removed": "Tika noņemts domēna pārvaldītājs %s", "domain_modified": "Izmaiņas domēnam %s ir saglabātas", "domain_removed": "Domēns %s ir noņemts", "eas_reset": "ActiveSync ierīces priekš lietotāja %s tika atiestatītas", @@ -545,7 +566,9 @@ "verified_yotp_login": "Apliecināta Yubico OTP pieteikšanās", "app_passwd_removed": "Noņemta lietotnes parole ar Id %s", "app_passwd_added": "Pievienota jauna lietotnes parole", - "f2b_banlist_refreshed": "Liegumu saraksta Id tika sekmīgi atsvaidzināts." + "f2b_banlist_refreshed": "Liegumu saraksta Id tika sekmīgi atsvaidzināts.", + "admin_added": "Tika pievienots pārvaldītājs %s", + "admin_removed": "Tika noņemts pārvaldītājs %s" }, "tfa": { "api_register": "%s izmanto Yubico Cloud API. Lūdzu iegūstiet API atslēgu priekš Jūsu atslēgashere", @@ -558,7 +581,7 @@ "key_id_totp": "Identifikators Jūsu atslēgai", "none": "Deaktivizēt", "scan_qr_code": "Lūdzu, skenējiet šo kodu ar savu autentifikācijas lietojumprogrammu vai ievadiet kodu manuāli.", - "select": "Lūdzu izvēlaties", + "select": "Lūgums atlasīt", "set_tfa": "Uzstādīt difi faktoru autentifik;acijas metodi", "tfa": "Divpakāpju pieteikšanās", "totp": "Uz laiku bāzēta vienreizēja parole (Google Autentifikātors utt.)", @@ -566,7 +589,8 @@ "waiting_usb_auth": "Gaida USB ierīci...

    Lūdzu, tagad nospiežiet pogu uz Jūsu WebAuthn USB ierīces.", "waiting_usb_register": "Gaida USB ierīci...

    Lūgums augstāk ievadīt savu paroli un apstiprināt reģistrēšanos ar USB ierīces pogas nospiešanu.", "yubi_otp": "Yubico OTP autentifikators", - "authenticators": "Autentificētāji" + "authenticators": "Autentificētāji", + "u2f_deprecated_important": "Lūgums reģistrēt savu atslēgu pārvaldības lapā ar jauno WebAuthn veidu." }, "user": { "action": "Rīcība", @@ -698,7 +722,8 @@ "warning": { "domain_added_sogo_failed": "Domēns pievienots, bet neizdevās pārsāknēt SOGO. Lūgums pārbaudīt servera žurnālus.", "dovecot_restart_failed": "Dovecot neizdevās pārsāknēties. Lūgums pārbaudīt žurnālus", - "is_not_primary_alias": "Izlaists aizstājvārds %s, kas nav galvenais" + "is_not_primary_alias": "Izlaists aizstājvārds %s, kas nav galvenais", + "no_active_admin": "Nevar deaktivēt pēdējo aktīvo pārvaldītāju" }, "oauth2": { "access_denied": "Lūgums pieteikties kā pastkastes īpašniekam, lai nodrošinātu piekļuvi ar OAuth2." diff --git a/data/web/lang/lang.nl-nl.json b/data/web/lang/lang.nl-nl.json index 71ec97209..6ed9e322a 100644 --- a/data/web/lang/lang.nl-nl.json +++ b/data/web/lang/lang.nl-nl.json @@ -26,7 +26,9 @@ "syncjobs": "Sync jobs", "tls_policy": "Versleutelingsbeleid", "unlimited_quota": "Onbeperkte quota voor mailboxen", - "domain_desc": "Wijzig domeinbeschrijving" + "domain_desc": "Wijzig domeinbeschrijving", + "pw_reset": "Toegang om mailcow gebruikers wachtwoord te resetten", + "domain_relayhost": "Verander relayhost voor een domein" }, "add": { "activate_filter_warn": "Alle andere filters worden gedeactiveerd zolang deze geactiveerd is.", @@ -104,7 +106,11 @@ "validate": "Verifieer", "validation_success": "Succesvol geverifieerd", "tags": "Tags", - "bcc_dest_format": "BCC-bestemming moet één geldig e-mailadres zijn.
    Als u een kopie naar meerdere adressen wilt sturen, maak dan een alias aan en gebruik die hier." + "bcc_dest_format": "BCC-bestemming moet één geldig e-mailadres zijn.
    Als u een kopie naar meerdere adressen wilt sturen, maak dan een alias aan en gebruik die hier.", + "dry": "Synchronisatie simuleren", + "internal": "Intern", + "internal_info": "Interne aliassen zijn alleen toegankelijk vanuit het eigen (alias)domein.", + "sender_allowed": "Toestaan om te verzenden als deze alias" }, "admin": { "access": "Toegang", @@ -138,7 +144,7 @@ "arrival_time": "Aankomsttijd", "authed_user": "Geauthenticeerde gebruiker", "ays": "Weet je zeker dat je deze actie wilt uitvoeren?", - "ban_list_info": "Bekijk de lijst met verbannen IP-adressen hieronder: netwerk (resterende tijd) - [acties].
    Rode labels geven een permanente verbanning aan.
    Het kan enkele seconden duren voordat wijzigingen hieronder zichtbaar zijn.", + "ban_list_info": "Bekijk de lijst met verbannen IP-adressen hieronder: netwerk (resterende tijd) - [acties].
    IP-adressen die in de wachtrij staan om te worden gedeblokkeerd, worden binnen enkele seconden uit de actieve banlijst verwijderd.
    \n
    Rode labels geven actieve permanente blokkades door denylisting aan.", "change_logo": "Logo", "configuration": "Instellingen", "convert_html_to_text": "Converteer HTML naar plaintext", @@ -171,7 +177,7 @@ "excludes": "Exclusief", "f2b_ban_time": "Verbanningstijd (s)", "f2b_ban_time_increment": "Verbanningstijd wordt verhoogd met elk verbanning", - "f2b_blacklist": "Netwerken/hosts op de blacklist", + "f2b_blacklist": "Netwerken/hosts op de denylist", "f2b_filter": "Regex-filters", "f2b_list_info": "Een host of netwerk op de blacklist staat altijd boven eenzelfde op de whitelist. Het doorvoeren van wijzigingen kan enkele seconden in beslag nemen.", "f2b_max_attempts": "Maximaal aantal pogingen", @@ -206,7 +212,7 @@ "link": "Link", "loading": "Even geduld aub...", "logo_info": "De afbeelding zal worden geschaald naar een hoogte van 40px voor de navigatiebar, en naar een breedte van 250px voor de startpagina.", - "lookup_mx": "Match bestemming aan MX (gebruik .outlook.com om alle mail gericht aan MX *.outlook.com over deze hop te laten gaan)", + "lookup_mx": "Bestemming is een reguliere expressie die wordt gematcht met de MX-naam (.*\\.google\\.com om alle e-mail die gericht is aan een MX die eindigt op google.com via deze hop te routeren)", "main_name": "\"Mailcow\"", "merged_vars_hint": "Grijze rijen zijn samengevoegd van vars.(local.)inc.php en kunnen niet worden gewijzigd.", "message": "Bericht", @@ -275,7 +281,7 @@ "rspamd_com_settings": "Een beschrijving voor deze instelling zal automatisch worden gegenereerd, gebruik de onderstaande presets als voorbeeld. Raadpleeg de Rspamd-documentatie voor meer informatie.", "rspamd_global_filters": "Globale filters", "rspamd_global_filters_agree": "Ik ben me ervan bewust dat aanpassingen desastreuze gevolgen kunnen hebben", - "rspamd_global_filters_info": "Ieder globaal filter heeft zijn eigen functie, zie de namen.", + "rspamd_global_filters_info": "Globale filtermaps bevatten verschillende soorten globale deny- en allowlists.", "rspamd_global_filters_regex": "De velden kunnen uitsluitend regular expressions bevatten met het formaat \"/pattern/options\", bijvoorbeeld /.+@domain\\.tld/i.
    Ondanks dat alle invoer wordt gecontroleerd op fouten, is het toch mogelijk dat Rspamd onbruikbaar wordt als deze de invoer niet kan lezen.
    Als je problemen ervaart, herstart Rspamd dan om de filters opnieuw te laten lezen.
    Elementen op de blacklist zijn uitgesloten van de quarantaine.", "rspamd_settings_map": "Rspamd", "sal_level": "Moo-level", @@ -337,7 +343,71 @@ "admins_ldap": "LDAP administrators", "api_read_only": "Alleen-lezen toegang", "api_read_write": "Lees en schrijf toegang", - "login_time": "Login tijd" + "login_time": "Login tijd", + "admin_quicklink": "Snelle link naar de admin-loginpagina verbergen", + "allowed_methods": "Toegestane toegangsmethoden", + "app_hide": "Verberg voor login", + "copy_to_clipboard": "Tekst gekopieerd naar het klembord!", + "login_page": "Loginpagina", + "domainadmin_quicklink": "Snelle link naar de domeinbeheerder-loginpagina verbergen", + "f2b_manage_external": "Fail2Ban extern beheren", + "f2b_manage_external_info": "Fail2Ban zal nog steeds de banlijst bijhouden, maar zal niet actief regels instellen om verkeer te blokkeren. Gebruik de onderstaande gegenereerde banlijst om het verkeer extern te blokkeren.", + "filter": "Filter", + "force_sso_text": "Als een externe OIDC-provider is geconfigureerd, verbergt deze optie de standaard Mailcow-loginformulieren en wordt alleen de Single SignOn-knop weergegeven.", + "force_sso": "Mailcow-login uitschakelen en alleen Single SignOn tonen", + "iam": "Identiteitsprovider", + "iam_attribute_field": "Attribuutveld", + "iam_authorize_url": "Autorisatie endpoint", + "iam_auth_flow": "Autorisatie flow", + "iam_auth_flow_info": "Naast de Authorization Code Flow (Standard Flow in Keycloak), die wordt gebruikt voor Single SignOn-login, ondersteunt Mailcow ook een authenticatiestroom met directe inloggegevens. De Mailpassword Flow probeert de gebruikersgegevens te valideren door gebruik te maken van de Keycloak Admin REST API. Mailcow haalt het gehashte wachtwoord op uit het attribuut mailcow_password, dat in Keycloak is gekoppeld.", + "iam_client_id": "Client ID", + "iam_client_secret": "Client Secret", + "iam_client_scopes": "Clientscopes", + "iam_default_template": "Standaardsjabloon", + "iam_default_template_description": "Als er geen sjabloon aan een gebruiker is toegewezen, wordt het standaardsjabloon gebruikt voor het aanmaken van de mailbox, maar niet voor het bijwerken van de mailbox.", + "iam_description": "Configureer een externe provider voor authenticatie. De mailboxen van gebruikers worden automatisch aangemaakt bij hun eerste login, mits er een attribuutkoppeling is ingesteld.", + "iam_extra_permission": "Om de volgende instellingen te laten werken, heeft de mailcow-client in Keycloak een service account nodig en de toestemming view-users.", + "iam_host": "Hostname", + "iam_host_info": "Voer één of meerdere LDAP-hosts in, gescheiden door komma’s.", + "iam_import_users": "Importeer gebruikers", + "iam_login_provisioning": "Gebruikers automatisch aanmaken bij het inloggen", + "iam_mapping": "Attribuutkoppeling", + "iam_bindpass": "Bind-wachtwoord", + "iam_periodic_full_sync": "Periodieke volledige synchronisatie", + "iam_port": "Poort", + "iam_realm": "Realm", + "iam_redirect_url": "Redirect-URL", + "iam_rest_flow": "Mailwachtwoord-flow", + "iam_server_url": "Server Url", + "iam_sso": "Single SignOn", + "iam_sync_interval": "Synchronisatie-/importinterval (min)", + "iam_test_connection": "Verbinding testen", + "iam_token_url": "Token endpoint", + "iam_userinfo_url": "Gebruikersinfo endpoint", + "iam_username_field": "Gebruikersnaam veld", + "iam_use_ssl": "Gebruik SSL", + "iam_use_ssl_info": "Als SSL wordt ingeschakeld en de poort is ingesteld op 389, wordt deze automatisch aangepast naar 636.", + "iam_use_tls": "Gebruik StartTLS", + "iam_use_tls_info": "Als TLS wordt ingeschakeld, moet je de standaardpoort voor je LDAP-server (389) gebruiken. SSL-poorten kunnen niet worden gebruikt.", + "iam_version": "Versie", + "ignore_ssl_error": "Negeer SSL foutmeldingen", + "ip_check_opt_in": "Kies ervoor om de externe diensten ipv4.mailcow.email en ipv6.mailcow.email te gebruiken om externe IP-adressen te bepalen.", + "needs_restart": "vereist een herstart", + "no": "✕", + "password_reset_info": "Als er geen herstel-e-mailadres is opgegeven, kan deze functie niet worden gebruikt.", + "password_reset_settings": "Instellingen voor wachtwoordherstel", + "password_reset_tmpl_html": "HTML sjabloon", + "password_reset_tmpl_text": "Tekst sjabloon", + "password_settings": "Wachtwoordinstellingen", + "quicklink_text": "Toon of verberg snelle links naar andere inlogpagina’s onder het inlogformulier", + "queue_unban": "deblokkeren", + "reset_password_vars": "{{link}} De gegenereerde link om het wachtwoord te resetten
    {{username}} De mailboxnaam van de gebruiker die het wachtwoordherstel heeft aangevraagd
    {{username2}} De naam van de herstelmailbox
    {{date}} De datum waarop het verzoek tot wachtwoordherstel is gedaan
    {{token_lifetime}} De geldigheidsduur van de token in minuten
    {{hostname}} De mailcow-hostnaam", + "restore_template": "Leeg laten om het standaardsjabloon te herstellen.", + "task": "Taak", + "transport_test_rcpt_info": "• Gebruik null@hosted.mailcow.de om relaying naar een externe bestemming te testen.", + "user_link": "Gebruikerslink", + "user_quicklink": "Snelle link naar de gebruikers-loginpagina verbergen", + "yes": "✓" }, "danger": { "access_denied": "Toegang geweigerd of ongeldige gegevens", @@ -463,7 +533,27 @@ "demo_mode_enabled": "Demo modus is ingeschakeld", "template_exists": "Sjabloon %s bestaat al", "template_id_invalid": "Sjabloon ID %s ongeldig", - "template_name_invalid": "Sjabloon naam ongeldig" + "template_name_invalid": "Sjabloon naam ongeldig", + "tfa_removal_blocked": "Twee-factor-authenticatie kan niet worden verwijderd omdat dit vereist is voor uw account.", + "authsource_in_use": "De identiteitsprovider kan niet worden gewijzigd of verwijderd omdat deze momenteel door één of meerdere gebruikers wordt gebruikt.", + "cors_invalid_origin": "Ongeldige Allow-Origin opgegeven", + "extended_sender_acl_denied": "Ontbrekende ACL om externe afzenderadressen in te stellen", + "generic_server_error": "Er is een onverwachte serverfout opgetreden. Neem contact op met uw beheerder.", + "iam_test_connection": "Verbinding mislukt", + "img_dimensions_exceeded": "De afbeelding overschrijdt de maximale afbeeldingsgrootte", + "img_size_exceeded": "De afbeelding overschrijdt de maximale afbeeldingsgrootte", + "invalid_reset_token": "Ongeldige reset-token", + "max_age_invalid": "Maximale leeftijd %s is ongeldig", + "mode_invalid": "Mode %s is ongeldig", + "mx_invalid": "MX-record %s is ongeldig", + "password_reset_invalid_user": "Mailbox niet gevonden of er is geen herstel-e-mailadres ingesteld", + "password_reset_na": "Wachtwoordherstel is momenteel niet beschikbaar. Neem contact op met uw beheerder.", + "recovery_email_failed": "Kon geen herstel-e-mail verzenden. Neem contact op met uw beheerder.", + "required_data_missing": "Vereiste gegevens %s ontbreken", + "reset_token_limit_exceeded": "De limiet voor reset-tokens is overschreden. Probeer het later opnieuw.", + "to_invalid": "Ontvanger mag niet leeg zijn", + "webauthn_username_failed": "De geselecteerde authenticator behoort tot een ander account", + "version_invalid": "Versie %s is ongeldig" }, "debug": { "chart_this_server": "Grafiek (deze server)", @@ -520,7 +610,7 @@ "alias": "Wijzig alias", "allow_from_smtp": "Sta enkel de volgende IP-adressen toe voor SMTP", "allow_from_smtp_info": "Laat leeg om alle afzenders toe te staan.
    IPv4/IPv6-adressen en netwerken.", - "allowed_protocols": "Toegestane protocollen", + "allowed_protocols": "Toegestane protocollen voor directe gebruikerstoegang (heeft geen invloed op protocollen voor app-wachtwoorden).", "app_name": "Naam van app", "app_passwd": "Appwachtwoord", "automap": "Probeer mappen automatisch te koppelen (\"Verzonden items\", \"Verzonden\" => \"Verzonden\" etc.)", @@ -605,7 +695,7 @@ "sogo_visible_info": "Wanneer verborgen zal een alias niet worden weergegeven als een selecteerbaar verzendadres. Deze optie beïnvloedt uitsluitend objecten die kunnen worden weergegeven in SOGo (gedeelde of niet-gedeelde aliasadressen die naar minstens één mailbox verwijzen).", "spam_alias": "Maak een nieuw tijdelijk alias aan, of pas deze aan", "spam_filter": "Spamfilter", - "spam_policy": "Voeg items toe of verwijder items van de white- of blacklist", + "spam_policy": "Voeg items toe of verwijder items van de deny- en allowlists", "spam_score": "Stel een aangepaste spamscore in", "subfolder2": "Synchroniseer in submap op bestemming
    (leeg = gebruik geen submappen)", "syncjob": "Wijzig sync job", @@ -624,7 +714,47 @@ "acl": "ACL (Toestemming)", "domain_footer": "Domeinbreede footer", "domain_footer_html": "HTML footer", - "mailbox_relayhost_info": "Wordt alleen toegepast op de mailbox en directe aliassen, maar heft een domein relayhost op." + "mailbox_relayhost_info": "Wordt alleen toegepast op de mailbox en directe aliassen, maar heft een domein relayhost op.", + "custom_attributes": "Aangepaste attributen", + "domain_footer_info_vars": { + "auth_user": "{= auth_user =} - Geauthenticeerde gebruikersnaam opgegeven door een MTA", + "from_user": "{= from_user =} - Afzendergedeelte van het e-mailadres, bijvoorbeeld bij \"moo@mailcow.tld\" wordt \"moo\" teruggegeven", + "from_name": "{= from_name =} - Naam van de afzender, bijvoorbeeld bij \"Mailcow \" wordt \"Mailcow\" teruggegeven", + "from_addr": "{= from_addr =} - Adres van de afzender", + "from_domain": "{= from_domain =} - Domein van de afzender", + "custom": "{= foo =} - Als de mailbox het aangepaste attribuut \"foo\" met de waarde \"bar\" heeft, wordt \"bar\" teruggegeven." + }, + "domain_footer_plain": "PLAIN-voettekst", + "domain_footer_skip_replies": "Voettekst negeren bij antwoord-e-mails", + "footer_exclude": "Uitsluiten van voettekst", + "internal": "Intern", + "internal_info": "Interne aliassen zijn alleen toegankelijk vanuit het eigen (alias)domein.", + "sender_allowed": "Toestaan om als deze alias te verzenden", + "sender_allowed_info": "Als dit is uitgeschakeld, kan deze alias alleen e-mail ontvangen. Gebruik de afzender-ACL om dit te overschrijven en specifieke mailboxen toestemming te geven om te verzenden.", + "lookup_mx": "Bestemming is een reguliere expressie die wordt gematcht met de MX-naam (.*.google.com om alle e-mail die gericht is aan een MX die eindigt op google.com via deze hop te routeren)", + "mailbox_rename": "Hernoem mailbox", + "mailbox_rename_agree": "Ik heb een backup gemaakt.", + "mailbox_rename_warning": "BELANGRIJK! Maak een back-up voordat u de mailbox hernoemt.", + "mailbox_rename_alias": "Alias automatisch aanmaken.", + "mailbox_rename_title": "Nieuwe naam van de lokale mailbox", + "mta_sts": "MTA-STS", + "mta_sts_info": "MTA-STS is een standaard die afdwingt dat e-mailbezorging tussen mailservers TLS met geldige certificaten gebruikt.
    \nDeze wordt gebruikt wanneer DANE niet mogelijk is vanwege ontbrekende of niet-ondersteunde DNSSEC.
    \nOpmerking: Als het ontvangende domein DANE met DNSSEC ondersteunt, heeft DANE altijd de voorkeur – MTA-STS fungeert alleen als een fallback.", + "mta_sts_version": "Versie", + "mta_sts_version_info": "Definieert de versie van de MTA-STS-standaard – momenteel is alleen STSv1 geldig.", + "mta_sts_mode": "Modus", + "mta_sts_mode_info": "Er zijn drie modus om uit te kiezen:
    • testing – het beleid wordt alleen gemonitord, overtredingen hebben geen impact.
    • enforce – het beleid wordt strikt afgedwongen, verbindingen zonder geldige TLS worden geweigerd.
    • none – het beleid wordt gepubliceerd maar niet toegepast.
    ", + "mta_sts_max_age": "Maximale leeftijd", + "mta_sts_max_age_info": "Tijd in seconden dat ontvangende mailservers dit beleid mogen cachen voordat het opnieuw wordt opgehaald.", + "mta_sts_mx": "MX server", + "mta_sts_mx_info": "Staat verzending alleen toe naar expliciet vermelde mailserver-hostnamen; de verzendende MTA controleert of de DNS MX-hostnaam overeenkomt met de beleidslijst en staat levering alleen toe met een geldig TLS-certificaat (beschermt tegen MITM).", + "mta_sts_mx_notice": "Meerdere MX-servers kunnen worden opgegeven (gescheiden door komma’s).", + "none_inherit": "Geen / Erven", + "password_recovery_email": "E-mail voor wachtwoordherstel", + "pushover": "Pushover", + "quota_warning_bcc": "Quotum waarschuwing BCC", + "quota_warning_bcc_info": "Waarschuwingen zullen in afzonderlijke exemplaren naar de volgende ontvangers worden gestuurd. Het onderwerp zal worden gevolgd door de bijbehorende gebruikersnaam tussen haakjes, bijvoorbeeld: Contingentwaarschuwing (user@example.com).", + "sogo_access": "Verleen directe logintoegang tot SOGo", + "sogo_access_info": "Na het inloggen wordt de gebruiker automatisch doorgestuurd naar SOGo" }, "footer": { "cancel": "Annuleren", @@ -637,18 +767,21 @@ "restart_container": "Herstart container", "restart_container_info": "Belangrijk: Een herstart kan enige tijd in beslag nemen, wacht aub totdat dit proces voltooid is.
    Deze pagina zal zichzelf verversen zodra het proces voltooid is.", "restart_now": "Nu herstarten", - "restarting_container": "Container wordt herstart, even geduld aub..." + "restarting_container": "Container wordt herstart, even geduld aub...", + "hibp_check": "Controleer tegen haveibeenpwned.com", + "nothing_selected": "Niets geselecteerd" }, "header": { "administration": "Configuratie & details", "apps": "Apps", - "debug": "Systeeminformatie", + "debug": "Informatie", "email": "E-Mail", "mailcow_config": "Beheer", "quarantine": "Quarantaine", "restart_netfilter": "Herstart netfilter", "restart_sogo": "Herstart SOGo", - "user_settings": "Gebruikersinstellingen" + "user_settings": "Gebruikersinstellingen", + "mailcow_system": "Systeem" }, "info": { "awaiting_tfa_confirmation": "In afwachting van tweefactorauthenticatie...", @@ -662,7 +795,22 @@ "mobileconfig_info": "Log in als mailboxgebruiker om het Apple-verbindingsprofiel te downloaden.", "other_logins": "of aanmelden met", "password": "Wachtwoord", - "username": "Gebruikersnaam" + "username": "Gebruikersnaam", + "back_to_mailcow": "Terug naar mailcow", + "forgot_password": "> Wachtwoord vergeten?", + "invalid_pass_reset_token": "De reset-wachtwoordtoken is ongeldig of verlopen.
    Vraag een nieuwe link voor het resetten van het wachtwoord aan.", + "login_linkstext": "Niet de juiste login?", + "login_usertext": "Log in als gebruiker", + "login_domainadmintext": "Login in als domeinbeheerder", + "login_admintext": "Log in als administrator", + "login_user": "Gebruikerslogin", + "login_dadmin": "Domeinbeheerder login", + "login_admin": "Administrator login", + "new_password": "Nieuw wachtwoord", + "new_password_confirm": "Bevestig nieuw wachtwoord", + "reset_password": "Herstel wachtwoord", + "request_reset_password": "Verzoek wachtwoord wijziging", + "email": "E-mailadres" }, "mailbox": { "action": "Handeling", @@ -782,7 +930,7 @@ "sieve_preset_5": "Autoreply (vakantie)", "sieve_preset_6": "Weiger mail met antwoord", "sieve_preset_7": "Forward en behoud/verwijder", - "sieve_preset_8": "Verwijder mail verstuurd naar een aliasadres van de afzender", + "sieve_preset_8": "E-mail van een specifieke afzender doorsturen, als gelezen markeren en in een submap plaatsen", "sieve_preset_header": "Zie de onderstaande voorbeelden. Raadpleeg Wikipedia voor meer informatie.", "sogo_visible": "Alias tonen in SOGo", "sogo_visible_n": "Verberg alias in SOGo", @@ -809,7 +957,37 @@ "toggle_all": "Selecteer alles", "username": "Gebruikersnaam", "waiting": "Wachten", - "weekly": "Wekelijks" + "weekly": "Wekelijks", + "add_alias_expand": "Alias uitbreiden over alias-domeinen", + "add_template": "Sjabloon toevoegen", + "all_domains": "Alle domeinen", + "catch_all": "Catch-All", + "created_on": "Aangemaakt op", + "domain_templates": "Domein sjablonen", + "domain_quota_total": "Totale domein-opslagruimte", + "goto_ham": "Leren als Ham", + "goto_spam": "Leren als spam", + "iam": "Identiteitsprovider", + "internal": "Intern", + "last_pw_change": "Laatste wachtwoordwijziging", + "mailbox_templates": "Mailbox sjablonen", + "no": "✕", + "open_logs": "Open logs", + "recipient": "Ontvanger", + "relay_unknown": "Onbekende mailboxen relayen", + "sender": "Afzender", + "syncjob_check_log": "Controleer log", + "syncjob_last_run_result": "Laatste uitvoeringsresultaat", + "syncjob_EX_OK": "Succes", + "syncjob_EXIT_CONNECTION_FAILURE": "Verbindingsprobleem", + "syncjob_EXIT_TLS_FAILURE": "Probleem met versleutelde verbinding", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authenticatieprobleem", + "syncjob_EXIT_OVERQUOTA": "Doel mailbox is over quota", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kan geen verbinding maken met de externe server", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Verkeerde gebruikersnaam of wachtwoord", + "templates": "Sjablonen", + "template": "Sjabloon", + "yes": "✓" }, "oauth2": { "access_denied": "Log in als een mailboxgebruiker om toegang via OAuth te verlenen", @@ -842,7 +1020,7 @@ "notified": "Verwittigd", "qhandler_success": "Aanvraag succesvol verzonden naar het systeem. Je kunt nu het venster sluiten.", "qid": "Rspamd QID", - "qinfo": "Het quarantainesysteem slaat een kopie van zowel geweigerde mail (voor de afzender zal het lijken alsof de mail niet afgeleverd is), als mail die afgeleverd is de spamfolder, op in de database.\r\n
    \"Markeer als spam en verwijder\" traint het systeem om soortgelijke mails in de toekomst opnieuw als spam te markeren.\r\n
    Wanneer er meerdere berichten tegelijkertijd worden behandeld kan het mogelijk enige tijd duren.
    Elementen op de blacklist zijn uitgesloten van de quarantaine.", + "qinfo": "Het quarantainesysteem slaat een kopie van zowel geweigerde mail (voor de afzender zal het lijken alsof de mail niet afgeleverd is), als mail die afgeleverd is de spamfolder, op in de database.\n
    \"Markeer als spam en verwijder\" traint het systeem om soortgelijke mails in de toekomst opnieuw als spam te markeren.\n
    Wanneer er meerdere berichten tegelijkertijd worden behandeld kan het mogelijk enige tijd duren.
    Elementen op de denylist zijn uitgesloten van de quarantaine.", "qitem": "Quarantaine-item", "quarantine": "Quarantaine", "quick_actions": "Handelingen", @@ -874,7 +1052,19 @@ "toggle_all": "Selecteer alles" }, "queue": { - "queue_manager": "Queue manager" + "queue_manager": "Queue manager", + "delete": "Verwijder alles", + "flush": "Wachtrij leegmaken", + "info": "De mailwachtrij bevat alle e-mails die wachten op aflevering. Als een e-mail lange tijd in de mailwachtrij blijft staan, wordt deze automatisch door het systeem verwijderd.
    De foutmelding van de betreffende e-mail geeft informatie over waarom de e-mail niet kon worden afgeleverd.", + "legend": "Functies voor acties in de mailwachtrij:", + "ays": "Bevestig dat u alle items uit de huidige wachtrij wilt verwijderen.", + "deliver_mail": "Afleveren", + "deliver_mail_legend": "Probeert de geselecteerde e-mails opnieuw te bezorgen.", + "hold_mail": "Vasthouden", + "hold_mail_legend": "Houdt de geselecteerde e-mails vast. (Voorkomt verdere afleverpogingen)", + "show_message": "Bericht weergeven", + "unban": "Wachtrij deblokkeren", + "unhold_mail": "Vrijgeven" }, "start": { "help": "Toon/verberg hulppaneel", @@ -994,7 +1184,8 @@ "start_fido2_validation": "Start FIDO2-validatie", "fido2_auth": "Aanmelden met FIDO2", "fido2_success": "Apparaat succesvol geregistreerd", - "fido2_validation_failed": "Validatie mislukt" + "fido2_validation_failed": "Validatie mislukt", + "set_fido2_touchid": "Registreer Touch ID op Apple M1" }, "user": { "action": "Handeling", @@ -1161,6 +1352,7 @@ "loadingRecords": "Laden...", "processing": "Wachten alstublieft..", "search": "Zoeken:", - "zeroRecords": "Geen overeenkomsten gevonden" + "zeroRecords": "Geen overeenkomsten gevonden", + "infoFiltered": "(gefilterd uit _MAX_ totale vermeldingen)" } } diff --git a/data/web/lang/lang.pl-pl.json b/data/web/lang/lang.pl-pl.json index 758d0cb40..de3557c03 100644 --- a/data/web/lang/lang.pl-pl.json +++ b/data/web/lang/lang.pl-pl.json @@ -189,7 +189,7 @@ "api_info": "API jest w trakcie prac. Dokumentację można znaleźć pod adresem /api", "api_key": "klucz API", "api_read_only": "Dostęp tylko do odczytu", - "api_read_write": "Dostęp tylko do odczytu", + "api_read_write": "Dostęp do odczytu i zapisu", "api_skip_ip_check": "Pomiń sprawdzenie IP dla API", "app_hide": "Ukryj dla logowania", "app_links": "Linki aplikacji", @@ -1226,7 +1226,7 @@ "decimal": ".", "emptyTable": "Brak danych w tabeli", "expand_all": "Rozszerz wszystko", - "info": "Wyświetlanie od START do END z TOTAL wpisów", + "info": "Wyświetlanie od _START_ do _END_ z _TOTAL_ wpisów", "infoEmpty": "Wyświetlanie od 0 do 0 z 0 wpisów", "infoFiltered": "(filtrowane z _MAX_ suma wpisów)", "thousands": ",", diff --git a/data/web/lang/lang.pt-br.json b/data/web/lang/lang.pt-br.json index b01d97656..5645537c6 100644 --- a/data/web/lang/lang.pt-br.json +++ b/data/web/lang/lang.pt-br.json @@ -38,7 +38,7 @@ "add_domain_only": "Adicionar somente domínio", "add_domain_restart": "Adicionar domínio e reiniciar o SoGo", "alias_address": "Endereço (s) de alias", - "alias_address_info": "Endereço/s de e-mail completo ou @example .com, para capturar todas as mensagens de um domínio (separadas por vírgula). somente domínios mailcow.", + "alias_address_info": "Endereço(s) de e-mail completo(s) ou @example.com, para capturar todas as mensagens de um domínio (separadas por vírgula). Apenas domínios do Mailcow.", "alias_domain": "Domínio de alias", "alias_domain_info": "Somente nomes de domínio válidos (separados por vírgula).", "app_name": "Nome do aplicativo", @@ -111,7 +111,8 @@ "validation_success": "Validado com sucesso", "dry": "Simular sincronização", "internal": "Interno", - "internal_info": "Aliases internos são acessíveis apenas a partir do próprio domínio ou alias de domínio." + "internal_info": "Aliases internos são acessíveis apenas a partir do próprio domínio ou alias de domínio.", + "sender_allowed": "Permitir enviar como este alias" }, "admin": { "access": "Acesso", @@ -151,13 +152,13 @@ "arrival_time": "Hora de chegada (hora do servidor)", "authed_user": "Usuário autoritário", "ays": "Tem certeza de que deseja continuar?", - "ban_list_info": "Veja uma lista de IPs banidos abaixo: rede (tempo restante de banimento) - [ações].
    Os IPs na fila para serem desbanidos serão removidos da lista de banimentos ativos em alguns segundos.
    Rótulos vermelhos indicam proibições permanentes ativas na lista negra.", + "ban_list_info": "Veja abaixo a lista de IPs banidos: rede (tempo restante do banimento) - [ações].
    IPs na fila para serem desbanidos serão removidos da lista de banimentos ativos em alguns segundos.
    Rótulos vermelhos indicam banimentos permanentes ativos na lista de bloqueio.", "change_logo": "Alterar logotipo", "logo_normal_label": "Normal", "logo_dark_label": "Invertido para o modo escuro", "configuration": "Configuração", "convert_html_to_text": "Converter HTML em texto sem formatação", - "copy_to_clipboard": "Text copied to clipboard!", + "copy_to_clipboard": "Copiado para a área de transferência!", "cors_settings": "Configurações do CORS", "credentials_transport_warning": "Aviso: Adicionar uma nova entrada no mapa de transporte atualizará as credenciais de todas as entradas com uma coluna correspondente do próximo salto.", "customer_id": "ID do cliente", @@ -188,9 +189,9 @@ "excludes": "Exclui esses destinatários", "f2b_ban_time": "Tempo (s) de proibição", "f2b_ban_time_increment": "O tempo de banimento é incrementado com cada banimento", - "f2b_blacklist": "Redes/hosts na lista negra", + "f2b_blacklist": "Redes/hosts na lista de bloqueio", "f2b_filter": "Filtros Regex", - "f2b_list_info": "Um host ou rede na lista negra sempre superará uma entidade na lista branca. As atualizações da lista levarão alguns segundos para serem aplicadas.", + "f2b_list_info": "Um host ou rede na lista de bloqueio sempre terá prioridade sobre uma entidade na lista de permissões. As atualizações das listas levarão alguns segundos para serem aplicadas.", "f2b_manage_external": "Gerenciar Fail2Ban externamente", "f2b_manage_external_info": "O Fail2ban ainda manterá a lista de banimentos, mas não definirá ativamente regras para bloquear o tráfego. Use a lista de banimento gerada abaixo para bloquear externamente o tráfego.", "f2b_max_attempts": "Máximo de tentativas", @@ -200,7 +201,7 @@ "f2b_parameters": "Parâmetros do Fail2ban", "f2b_regex_info": "Registros considerados: SoGo, Postfix, Dovecot, PHP-FPM.", "f2b_retry_window": "Repita a (s) janela (s) para o máximo de tentativas", - "f2b_whitelist": "Redes/hosts incluídos na lista branca", + "f2b_whitelist": "Redes/hosts na lista de permissões", "filter_table": "Tabela de filtros", "forwarding_hosts": "Anfitriões de encaminhamento", "forwarding_hosts_add_hint": "Você pode especificar endereços IPv4/IPv6, redes em notação CIDR, nomes de host (que serão resolvidos para endereços IP) ou nomes de domínio (que serão resolvidos para endereços IP consultando registros SPF ou, na ausência deles, registros MX).", @@ -222,7 +223,7 @@ "includes": "Inclua esses destinatários", "ip_check": "Verificação de IP", "ip_check_disabled": "A verificação de IP está desativada. Você pode ativá-lo em
    Sistema > Configuração > Opções > Personalizar", - "ip_check_opt_in": "Opte por usar o serviço de terceiros ipv4.mailcow.email. e ipv6.mailcow.email para resolver endereços IP externos.", + "ip_check_opt_in": "Opte por usar o serviço de terceiros ipv4.mailcow.email e ipv6.mailcow.email para resolver endereços IP externos.", "is_mx_based": "Baseado em MX", "last_applied": "Aplicado pela última vez", "license_info": "Uma licença não é necessária, mas ajuda no desenvolvimento.
    Registre seu GUID aqui ou comprar suporte para sua instalação de mailcow.", @@ -244,7 +245,7 @@ "oauth2_add_client": "Adicionar cliente OAuth2", "oauth2_client_id": "ID do cliente", "oauth2_client_secret": "Segredo do cliente", - "oauth2_info": "A implementação OAuth2 suporta o tipo de concessão \"Código de Autorização\" e emite tokens de atualização.
    \nO servidor também emite automaticamente novos tokens de atualização, depois que um token de atualização foi usado.

    \n• O escopo padrão é perfil. Somente usuários com caixa de e-mail podem ser autenticados contra o OAuth2. Se o parâmetro de escopo for omitido, ele voltará para perfil.
    \nCaminhos para solicitações OAuth2 API:
    \n
      \n
    • Endpoint de autorização: /oauth/authorize
    • \n
    • Endpoint token: /oauth/token
    • \n
    • Página de recursos: /oauth/profile
    • \n
    \nRegenerar o segredo do cliente não expirará os códigos de autorização existentes, mas eles não renovarão seu token.

    \nA revogação dos tokens do cliente causará o término imediato de todas as sessões ativas. Todos os clientes precisam se autenticar novamente.", + "oauth2_info": "A implementação do OAuth2 suporta o tipo de concessão \"Código de Autorização\" e emite tokens de atualização.
    \nO servidor também emite automaticamente novos tokens de atualização após um token de atualização ter sido usado.
    \nO escopo padrão é profile. Somente usuários de caixa de correio podem ser autenticados com o OAuth2. Se o parâmetro de escopo for omitido, o padrão será profile.
    \nO parâmetro state deve ser enviado pelo cliente como parte da solicitação de autorização.

    \nCaminhos para solicitações à API OAuth2:
    \n\n
      \n
    • Ponto de extremidade de autorização: /oauth/authorize
    • \n
    • Endpoint do token: /oauth/token
    • \n
    • Página de recursos: /oauth/profile
    • \n
    \n\nRegenerar o segredo do cliente não expirará os códigos de autorização existentes, mas impedirá a renovação do token.

    \nA revogação dos tokens de cliente causará o encerramento imediato de todas as sessões ativas. Todos os clientes precisarão se autenticar novamente.", "oauth2_redirect_uri": "URI de redirecionamento", "oauth2_renew_secret": "Gere um novo segredo de cliente", "oauth2_revoke_tokens": "Revogar todos os tokens do cliente", @@ -320,8 +321,8 @@ "rspamd_com_settings": "Um nome de configuração será gerado automaticamente, veja os exemplos de predefinições abaixo. Para obter mais detalhes, consulte a documentação do Rspamd", "rspamd_global_filters": "Mapas de filtro globais", "rspamd_global_filters_agree": "Eu vou ter cuidado!", - "rspamd_global_filters_info": "Os mapas de filtros globais contêm diferentes tipos de listas negras e brancas globais.", - "rspamd_global_filters_regex": "Seus nomes explicam seu propósito. Todo o conteúdo deve conter uma expressão regular válida no formato “/padrão/opções” (por exemplo, /. + @domain\\ .tld/i).
    \r\n Embora verificações rudimentares estejam sendo executadas em cada linha de regex, a funcionalidade do Rspamd pode ser interrompida se não conseguir ler a sintaxe corretamente.
    \r\n O Rspamd tentará ler o conteúdo do mapa quando alterado. Se você tiver problemas, reinicie o Rspamd para forçar o recarregamento do mapa.
    Os elementos da lista negra são excluídos da quarentena.", + "rspamd_global_filters_info": "Os mapas de filtros globais contêm diferentes tipos de listas globais de bloqueio e de permissões.", + "rspamd_global_filters_regex": "Os nomes explicam sua finalidade. Todo o conteúdo deve conter uma expressão regular válida no formato \"/padrão/opções\" (por exemplo, /.[+@domain.tld](mailto:+@domain.tld)/i).
    \nEmbora verificações básicas sejam executadas em cada linha de regex, a funcionalidade do Rspamd pode ser comprometida se ele não conseguir ler a sintaxe corretamente.
    \nO Rspamd tentará ler o conteúdo do mapa quando ele for alterado. Se você tiver problemas, reinicie o Rspamd para forçar o recarregamento do mapa.
    Elementos na lista de bloqueio são excluídos da quarentena.", "rspamd_settings_map": "Mapa de configurações do Rspamd", "sal_level": "Nível de humor", "save": "Salvar alterações", @@ -556,7 +557,9 @@ "mode_invalid": "Modo %s é inválido", "mx_invalid": "Registro MX %s é inválido", "required_data_missing": "Dados obrigatórios %s estão ausentes", - "version_invalid": "Versão %s é inválida" + "version_invalid": "Versão %s é inválida", + "tfa_removal_blocked": "A autenticação de dois fatores não pode ser removida, pois é obrigatória para a sua conta.", + "quarantine_category_invalid": "A categoria da quarentena deve ser uma das seguintes: add_header, reject, all" }, "datatables": { "collapse_all": "Recolher tudo", @@ -727,7 +730,7 @@ "pushover_verify": "Verifique as credenciais", "quota_mb": "Cota (MiB)", "quota_warning_bcc": "Aviso de cota BCC", - "quota_warning_bcc_info": "Os avisos serão enviados em cópias separadas para os seguintes destinatários. O assunto será sufixado pelo nome de usuário correspondente entre colchetes, por exemplo: Aviso de cota (user@example.com).", + "quota_warning_bcc_info": "Os avisos serão enviados em cópias separadas para os seguintes destinatários. O assunto será precedido pelo nome de usuário correspondente entre colchetes, por exemplo: Aviso de cota (user@example.com).", "ratelimit": "Limite de taxa", "redirect_uri": "URL de redirecionamento/retorno de chamada", "relay_all": "Retransmita todos os destinatários", @@ -746,17 +749,17 @@ "sieve_desc": "Breve descrição", "sieve_type": "Tipo de filtro", "skipcrossduplicates": "Ignore mensagens duplicadas entre pastas (primeiro a chegar, primeiro a ser servido)", - "sogo_access": "Encaminhamento direto para o SOGoo", - "sogo_access_info": "Depois de fazer login, o usuário é automaticamente redirecionado para o SOGo.", + "sogo_access": "Encaminhamento direto para o SOGo", + "sogo_access_info": "Após o login, o usuário é automaticamente redirecionado para o SOGo.", "sogo_visible": "O alias é visível no SoGo", "sogo_visible_info": "Essa opção afeta somente objetos, que podem ser exibidos no SoGo (endereços de alias compartilhados ou não compartilhados apontando para pelo menos uma mailbox local). Se estiver oculto, um alias não aparecerá como remetente selecionável no SoGo.", "spam_alias": "Crie ou altere endereços de alias com limite de tempo", "spam_filter": "Filtro de spam", - "spam_policy": "Adicionar ou remover itens da lista branca/negra", + "spam_policy": "Adicionar ou remover itens da lista de permissões/bloqueio", "spam_score": "Defina uma pontuação de spam personalizada", "subfolder2": "Sincronizar na subpasta no destino
    (vazio = não usar subpasta)", "syncjob": "Editar tarefa de sincronização", - "target_address": "Ir para endereço/es (separados por vírgula)", + "target_address": "Ir para o(s) endereço(s) (separados por vírgula)", "target_domain": "Domínio de destino", "timeout1": "Tempo limite para conexão com o host remoto", "timeout2": "Tempo limite para conexão com o host local", @@ -781,7 +784,10 @@ "mta_sts_max_age_info": "Tempo em segundos que servidores de email de recepção podem armazenar esta política em cache até buscar novamente.", "mta_sts_mx": "Servidor MX", "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)." + "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.", + "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", @@ -802,7 +808,7 @@ "cancel": "Cancelar", "confirm_delete": "Confirme a exclusão", "delete_now": "Excluir agora", - "delete_these_items": "Confirme suas alterações no seguinte ID de objeto", + "delete_these_items": "Por favor, confirme as alterações feitas no seguinte ID de objeto.", "hibp_check": "Verifique em haveibeenpwned.com", "hibp_nok": "Combinado! Essa é uma senha potencialmente perigosa!", "hibp_ok": "Nenhuma combinação encontrada.", @@ -970,7 +976,7 @@ "recipient_map_new": "Novo destinatário", "recipient_map_new_info": "O destino do mapa do destinatário deve ser um endereço de e-mail válido ou um nome de domínio.", "recipient_map_old": "Destinatário original", - "recipient_map_old_info": "O destino original do mapa de um destinatário deve ser um endereço de e-mail válido ou um nome de domínio.", + "recipient_map_old_info": "O destino original do mapa de destinatário deve ser um endereço de e-mail válido ou um nome de domínio.", "recipient_maps": "Mapas de destinatários", "relay_all": "Retransmita todos os destinatários", "relay_unknown": "Retransmitir mailboxes desconhecidas", @@ -1029,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.", @@ -1061,7 +1068,7 @@ "notified": "Notificado", "qhandler_success": "Solicitação enviada com sucesso para o sistema. Agora você pode fechar a janela.", "qid": "Respand AID", - "qinfo": "O sistema de quarentena salvará as mensagens rejeitadas no banco de dados (o remetente não terá a impressão de uma mensagem entregue), bem como as mensagens, que são entregues como cópia na pasta Lixo eletrônico de uma mailbox.\n
    “Aprenda como spam e exclua” aprenderá uma mensagem como spam por meio do Teorema de Bayes e também calculará hashes difusos para negar mensagens semelhantes no futuro.\n
    Esteja ciente de que aprender várias mensagens pode ser demorado, dependendo do seu sistema.
    Os elementos da lista negra são excluídos da quarentena.", + "qinfo": "O sistema de quarentena irá salvar no banco de dados os e-mails rejeitados (o remetente não terá a impressão de que o e-mail foi entregue), bem como os e-mails que são entregues como cópia na pasta de spam de uma mailbox.\n
    “Aprender como spam e excluir” irá classificar a mensagem como spam por meio do Teorema de Bayes e também calcular hashes fuzzy para bloquear mensagens semelhantes no futuro.\n
    Esteja ciente de que o aprendizado de múltiplas mensagens pode, dependendo do seu sistema, levar tempo.
    Elementos na lista de bloqueio são excluídos da quarentena.", "qitem": "Item de quarentena", "quarantine": "Quarentena", "quick_actions": "Ações", @@ -1081,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
    Tamanho máximo do e-mail: %s MiB", + "settings_info": "Número máximo de elementos em quarentena (por caixa de correio): %s
    Tamanho máximo do e-mail: %s MiB", "show_item": "Mostrar item", "spam": "Spam", "spam_score": "Ponto", @@ -1237,7 +1244,12 @@ "webauthn": "Autenticação WebAuthn", "waiting_usb_auth": "Aguardando o dispositivo USB...

    Toque no botão no seu dispositivo USB agora.", "waiting_usb_register": "Aguardando o dispositivo USB...

    Digite sua senha acima e confirme seu registro tocando no botão no seu dispositivo USB.", - "yubi_otp": "Autenticação Yubico OTP" + "yubi_otp": "Autenticação Yubico OTP", + "force_tfa": "Forçar o cadastro de 2FA no login", + "force_tfa_info": "O usuário será obrigado a configurar a autenticação de dois fatores antes de acessar o painel.", + "setup_title": "Autenticação de Dois Fatores Obrigatória", + "setup_required": "Sua conta exige autenticação de dois fatores. Configure um método de 2FA para continuar.", + "cancel_setup": "Cancelar e sair" }, "user": { "action": "Ação", @@ -1276,7 +1288,7 @@ "delete_ays": "Confirme o processo de exclusão.", "direct_aliases": "Endereços de alias diretos", "direct_aliases_desc": "Os endereços de alias diretos são afetados pelo filtro de spam e pelas configurações da política TLS.", - "direct_protocol_access": "Esse usuário da mailbox tem acesso externo direto aos seguintes protocolos e aplicativos. Essa configuração é controlada pelo administrador. As senhas de aplicativos podem ser criadas para conceder acesso a protocolos e aplicativos individuais.
    O botão “Login no webmail” fornece login único no SoGo e está sempre disponível.", + "direct_protocol_access": "Este usuário da mailbox possui acesso direto e externo aos seguintes protocolos e aplicações. Esta configuração é controlada pelo seu administrador. Senhas de aplicativo podem ser criadas para conceder acesso a protocolos e aplicações específicos.
    O botão “Webmail” fornece login único (SSO) para o SOGo e está sempre disponível.", "eas_reset": "Redefinir o cache do dispositivo ActiveSync", "eas_reset_help": "Em muitos casos, uma redefinição do cache do dispositivo ajudará a recuperar um perfil quebrado do ActiveSync.
    Atenção: Todos os elementos serão baixados novamente!", "eas_reset_now": "Reinicie agora", @@ -1352,12 +1364,12 @@ "sogo_profile_reset": "Redefinir perfil SoGo", "sogo_profile_reset_help": "Isso destruirá o perfil SoGo de um usuário e excluirá todos os dados de contato e calendário irrecuperáveis.", "sogo_profile_reset_now": "Redefina o perfil agora", - "spam_aliases": "Aliases de e-mail temporários", + "spam_aliases": "Aliases de e-mail de spam", "spam_score_reset": "Redefinir para o padrão do servidor", "spamfilter": "Filtro de spam", "spamfilter_behavior": "Avaliação", - "spamfilter_bl": "Lista negra", - "spamfilter_bl_desc": "Endereços de e-mail na lista negra para sempre serem classificados como spam e rejeitados. E-mails rejeitados não serão copiados para a quarentena. Podem ser usados curingas. Um filtro só é aplicado a aliases diretos (aliases com uma única caixa de correio de destino), excluindo aliases abrangentes e a própria mailbox.", + "spamfilter_bl": "Lista de bloqueio", + "spamfilter_bl_desc": "Endereços de e-mail na lista de bloqueio para sempre classificar como spam e rejeitados. E-mails rejeitados não serão copiados para a quarentena. Coringas podem ser utilizados. O filtro é aplicado apenas a aliases diretos (aliases com uma única mailbox de destino), excluindo aliases catch-all e a própria mailbox.", "spamfilter_default_score": "Valores padrão", "spamfilter_green": "Verde: esta mensagem não é spam", "spamfilter_hint": "O primeiro valor descreve a “pontuação baixa de spam”, o segundo representa a “alta pontuação de spam”.", @@ -1368,8 +1380,8 @@ "spamfilter_table_empty": "Não há dados para exibir", "spamfilter_table_remove": "remover", "spamfilter_table_rule": "Regra", - "spamfilter_wl": "Lista branca", - "spamfilter_wl_desc": "Os endereços de e-mail incluídos na lista branca são programados para nunca serem classificados como spam. Podem ser usados curingas. Um filtro só é aplicado a aliases diretos (aliases com uma única mailbox de destino), excluindo aliases abrangentes e a própria mailbox.", + "spamfilter_wl": "Lista de permissões", + "spamfilter_wl_desc": "Endereços de e-mail na lista de permissões são configurados para nunca serem classificados como spam. Coringas podem ser utilizados. O filtro é aplicado apenas a aliases diretos (aliases com uma única mailbox de destino), excluindo aliases catch-all e a própria mailbox.", "spamfilter_yellow": "Amarelo: esta mensagem pode ser spam, será marcada como spam e movida para sua pasta de lixo eletrônico", "status": "Status", "sync_jobs": "Trabalhos de sincronização", @@ -1383,7 +1395,7 @@ "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Não é possível se conectar ao servidor remoto", "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nome de usuário ou senha incorretos", "tag_handling": "Definir o tratamento para e-mails marcados", - "tag_help_example": "Exemplo de um endereço de e-mail marcado: me +Facebook @example .org", + "tag_help_example": "Exemplo de endereço de e-mail com tag: eu+Facebook@exemplo.org", "tag_help_explain": "Na subpasta: uma nova subpasta com o nome da tag será criada abaixo da CAIXA DE ENTRADA (“Caixa de entrada/Facebook”).
    \r\nNo assunto: o nome das tags será anexado ao assunto do e-mail, por exemplo: “[Facebook] Minhas notícias”.", "tag_in_none": "Não faça nada", "tag_in_subfolder": "Na subpasta", @@ -1408,7 +1420,11 @@ "authentication": "Autenticação", "overview": "Visão geral", "protocols": "Protocolos", - "tfa_info": "A autenticação de dois fatores ajuda a proteger sua conta. Se você habilitá-la, precisará de senhas de aplicativo para fazer login em aplicativos ou serviços que não suportam autenticação de dois fatores (por exemplo, clientes de email)." + "tfa_info": "A autenticação de dois fatores ajuda a proteger sua conta. Se você habilitá-la, precisará de senhas de aplicativo para fazer login em aplicativos ou serviços que não suportam autenticação de dois fatores (por exemplo, clientes de email).", + "expire_never": "Nunca expirar", + "forever": "Para sempre", + "pw_update_required": "Sua conta exige a alteração de senha. Defina uma nova senha para continuar.", + "spam_aliases_info": "Um alias de spam é um endereço de e-mail temporário que pode ser usado para proteger endereços de e-mail reais.
    Opcionalmente, pode-se definir um tempo de expiração para que o alias seja automaticamente desativado após o período definido, descartando efetivamente endereços que tenham sido abusados ou vazados." }, "warning": { "cannot_delete_self": "Não é possível excluir o usuário conectado", diff --git a/data/web/lang/lang.ru-ru.json b/data/web/lang/lang.ru-ru.json index e7d825639..58353fb5c 100644 --- a/data/web/lang/lang.ru-ru.json +++ b/data/web/lang/lang.ru-ru.json @@ -111,7 +111,8 @@ "validate": "Проверить", "validation_success": "Проверка прошла успешно", "internal": "Внутренний", - "internal_info": "Внутренние псевдонимы доступны только из самого домена или доменов-псевдонимов." + "internal_info": "Внутренние псевдонимы доступны только из самого домена или доменов-псевдонимов.", + "sender_allowed": "Разрешить отправку с этим псевдонимом" }, "admin": { "access": "Настройки доступа", @@ -781,7 +782,9 @@ "mta_sts_max_age_info": "Время в секундах, в течение которого принимающие почтовые серверы могут кэшировать эту политику перед повторной загрузкой.", "mta_sts_mx": "Сервер MX", "mta_sts_mx_info": "Разрешает отправку только на явно указанные имена хостов почтовых серверов; отправляющий MTA проверяет, соответствует ли DNS-имя MX-хоста списку политик, и разрешает доставку только с подлинным TLS-сертификатом (защита от MITM).", - "mta_sts_mx_notice": "Можно указать несколько MX-серверов (через запятую)." + "mta_sts_mx_notice": "Можно указать несколько MX-серверов (через запятую).", + "sender_allowed": "Разрешить отправку с этим псевдонимом", + "sender_allowed_info": "Если отключено, этот псевдоним может только принимать почту. Используйте ACL отправителя, чтобы переопределить и предоставить определенным почтовым ящикам разрешение на отправку." }, "fido2": { "confirm": "Подтвердить", diff --git a/data/web/lang/lang.si-si.json b/data/web/lang/lang.si-si.json index 918fb8b1a..c13a08907 100644 --- a/data/web/lang/lang.si-si.json +++ b/data/web/lang/lang.si-si.json @@ -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", @@ -111,7 +112,8 @@ "timeout2": "Časovna omejitev za povezavo do lokalnega gostitelja", "dry": "Simuliraj sinhronizacijo", "internal": "Notranje", - "internal_info": "Notranji vzdevki so dostopni samo iz lastne domene ali vzdevkov domen." + "internal_info": "Notranji vzdevki so dostopni samo iz lastne domene ali vzdevkov domen.", + "sender_allowed": "Dovoli pošiljanje kot ta vzdevek" }, "admin": { "access": "Dostop", @@ -411,7 +413,12 @@ "task": "Naloga", "user_link": "Uporabniška povezava", "iam_realm": "Realm", - "needs_restart": "potreben je ponovni zagon" + "needs_restart": "potreben je ponovni zagon", + "ui_theme": "Tema", + "ui_theme_dark": "Vključuje različico s temnim načinom", + "ui_theme_default": "Privzeto", + "ui_theme_info": "Teme so mape v data/web/css/themes/<name>/, ki vsebujejo naslednje datoteke:
    • theme.css (obvezna)
    • dark.css (neobvezna, temni način)
    • bootstrap.css (neobvezna, nadomesti osnovno temo Bootstrap)
    • theme.json (neobvezna, name in description)
    ", + "ui_theme_none": "Tem ni bilo mogoče najti." }, "danger": { "alias_goto_identical": "Vzdevek in ciljni naslov se ne smeta ujemati", @@ -556,7 +563,11 @@ "max_age_invalid": "Najvišja starost %s je neveljavna", "mode_invalid": "Način %s ni veljaven", "mx_invalid": "Zapis MX %s je neveljaven", - "version_invalid": "Različica %s je neveljavna" + "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", + "external_goto_denied": "Zunanji naslov goto %s ni dovoljen", + "ui_theme_invalid": "Tema ni najdena" }, "debug": { "containers_info": "Informacije o zabojniku", @@ -780,7 +791,10 @@ "mta_sts_mx_info": "Omogoča pošiljanje samo na izrecno navedena imena gostiteljskih strežnikov poštnih strežnikov; pošiljajoči MTA preveri, ali se ime gostitelja DNS MX ujema s seznamom pravilnikov, in dovoljuje dostavo le z veljavnim potrdilom TLS (zaščita pred MITM).", "mta_sts_mx_notice": "Določiti je mogoče več strežnikov MX (ločenih z vejicami).", "internal": "Notranje", - "internal_info": "Notranji vzdevki so dostopni samo iz lastne domene ali vzdevkov domen." + "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.", + "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": "Pomembno: Eleganten ponovni zagon lahko traja nekaj časa, zato počakajte, da se konča.", @@ -996,7 +1010,8 @@ "weekly": "Tedensko", "sieve_info": "Na uporabnika lahko shranite več filtrov, vendar je lahko hkrati aktiven le en predfilter in en postfilter.
    \nVsak filter bo obdelan v opisanem vrstnem redu. Niti neuspešen skript niti izdan ukaz »keep;« ne bosta ustavila obdelave nadaljnjih skript. Spremembe globalnih skriptov sita bodo sprožile ponovni zagon Dovecota.

    Globalni predfilter sita • Predfilter • Uporabniški skripti • Postfilter • Globalni postfilter sita", "tls_policy_maps_info": "Ta preslikava pravilnikov preglasi pravila odhodnega prenosa TLS neodvisno od uporabnikovih nastavitev pravilnikov TLS.
    \n Za več informacij preverite dokumentacijo »smtp_tls_policy_maps«.", - "internal": "Notranje" + "internal": "Notranje", + "force_tfa": "TFA" }, "fido2": { "known_ids": "Znani ID-ji", @@ -1072,7 +1087,7 @@ "rspamd_result": "Rezultat Rspamd", "sender": "Pošiljatelj (SMTP)", "sender_header": "Pošiljatelj (glava »Od«)", - "settings_info": "Največje število elementov za karanteno: %s
    Največja velikost e-pošte: %s MiB", + "settings_info": "Največje število elementov za karanteno (na poštni predal): %s
    Največja velikost e-pošte: %s MiB", "show_item": "Prikaži element", "spam": "Neželena pošta", "subj": "Zadeva", @@ -1197,7 +1212,8 @@ "cors_headers_edited": "Nastavitve CORS so bile shranjene", "item_released": "Izdan je bil element %s", "saved_settings": "Shranjene nastavitve", - "learned_ham": "Uspešno naučen ID %s kot zaželjen" + "learned_ham": "Uspešno naučen ID %s kot zaželjen", + "ui_theme_modified": "Tema je bila uspešno shranjena" }, "start": { "imap_smtp_server_auth_info": "Prosimo, uporabite svoj celoten e-poštni naslov in mehanizem za preverjanje pristnosti PLAIN.
    \nVaši podatki za prijavo bodo šifrirani z obveznim šifriranjem na strani strežnika.", @@ -1229,7 +1245,12 @@ "u2f_deprecated_important": "Prosimo, registrirajte svoj ključ v skrbniški plošči z novo metodo WebAuthn.", "waiting_usb_auth": "Čakanje na napravo USB ...

    Zdaj se dotaknite gumba na napravi USB.", "waiting_usb_register": "Čakanje na napravo USB ...

    Vnesite svoje geslo zgoraj in potrdite registracijo tako, da tapnete gumb na napravi USB.", - "yubi_otp": "Avtentikacija z enkratnim geslom Yubico" + "yubi_otp": "Avtentikacija z enkratnim geslom Yubico", + "force_tfa": "Vsiljena registracija 2FA ob prijavi", + "force_tfa_info": "Uporabnik bo moral pred dostopom do nadzorne plošče nastaviti dvofaktorsko overjanje.", + "setup_title": "Zahtevana je dvofaktorska avtentikacija", + "setup_required": "Vaš račun zahteva dvofaktorsko overjanje. Za nadaljevanje nastavite metodo dvofaktorske overitve.", + "cancel_setup": "Prekliči in se odjavi" }, "ratelimit": { "disabled": "Onemogočeno", @@ -1410,7 +1431,8 @@ "syncjob_EX_OK": "Uspeh", "expire_never": "Nikoli ne poteče", "forever": "Za vedno", - "spam_aliases_info": "Vzdevek za neželeno pošto je začasni e-poštni naslov, ki ga je mogoče uporabiti za zaščito pravih e-poštnih naslovov.
    Po želji je mogoče nastaviti čas poteka veljavnosti, tako da se vzdevek po določenem obdobju samodejno deaktivira, s čimer se učinkovito znebite zlorabljenih ali razkritih naslovov." + "spam_aliases_info": "Vzdevek za neželeno pošto je začasni e-poštni naslov, ki ga je mogoče uporabiti za zaščito pravih e-poštnih naslovov.
    Po želji je mogoče nastaviti čas poteka veljavnosti, tako da se vzdevek po določenem obdobju samodejno deaktivira, s čimer se učinkovito znebite zlorabljenih ali razkritih naslovov.", + "pw_update_required": "Za vaš račun je potrebna sprememba gesla. Za nadaljevanje nastavite novo geslo." }, "warning": { "cannot_delete_self": "Prijavljenega uporabnika ni mogoče izbrisati", diff --git a/data/web/lang/lang.uz-uz.json b/data/web/lang/lang.uz-uz.json new file mode 100644 index 000000000..a8d2ecf82 --- /dev/null +++ b/data/web/lang/lang.uz-uz.json @@ -0,0 +1,1440 @@ +{ + "acl": { + "alias_domains": "Taxallus domenlarini qoʻshish", + "app_passwds": "Ilova parollarini boshqarish", + "bcc_maps": "BCC xaritalari", + "delimiter_action": "Ajratuvchi amali", + "domain_desc": "Domen tavsifini oʻzgartirish", + "domain_relayhost": "Domen uchun relay xostni oʻzgartirish", + "eas_reset": "EAS qurilmalarini tiklash", + "extend_sender_acl": "Tashqi manzillar yordamida joʻnatuvchi ACL ni kengaytirishga ruxsat berish", + "filters": "Filtrlar", + "login_as": "Pochta qutisi foydalanuvchisi sifatida kirish", + "mailbox_relayhost": "Pochta qutisi uchun relay xostni oʻzgartirish", + "prohibited": "ACL tomonidan taqiqlangan", + "protocol_access": "Protokolga kirishni oʻzgartirish", + "pushover": "Pushover", + "pw_reset": "mailcow foydalanuvchi parolini tiklashga ruxsat berish", + "quarantine": "Karantin amallari", + "quarantine_attachments": "Karantindagi ilovalar", + "quarantine_category": "Karantin bildirishnoma turkumini oʻzgartirish", + "quarantine_notification": "Karantin bildirishnomalarini oʻzgartirish", + "ratelimit": "Tezlik chegarasi", + "recipient_maps": "Qabul qiluvchi xaritalari", + "smtp_ip_access": "SMTP uchun ruxsat etilgan xostlarni oʻzgartirish", + "sogo_access": "SOGo kirishini boshqarishga ruxsat berish", + "sogo_profile_reset": "SOGo profilini tiklash", + "spam_alias": "Vaqtinchalik taxalluslar", + "spam_policy": "Qora roʻyxat/Oq roʻyxat", + "spam_score": "Spam bahosi", + "syncjobs": "Sinxronizatsiya vazifalari", + "tls_policy": "TLS siyosati", + "unlimited_quota": "Pochta qutilari uchun cheksiz kvota" + }, + "add": { + "activate_filter_warn": "Faol belgilansa, boshqa barcha filtrlar oʻchirib qoʻyiladi.", + "active": "Faol", + "add": "Qoʻshish", + "add_domain_only": "Faqat domen qoʻshish", + "add_domain_restart": "Domen qoʻshish va SOGoni qayta ishga tushirish", + "alias_address": "Taxallus manzil(lar)i", + "alias_address_info": "Toʻliq e-pochta manzil(lar)i yoki @example.com — domenga keladigan barcha xabarlarni qabul qilish uchun (vergul bilan ajratilgan). Faqat mailcow domenlari.", + "alias_domain": "Taxallus domeni", + "alias_domain_info": "Faqat haqiqiy domen nomlari (vergul bilan ajratilgan).", + "app_name": "Ilova nomi", + "app_password": "Ilova paroli qoʻshish", + "app_passwd_protocols": "Ilova paroli uchun ruxsat etilgan protokollar", + "automap": "Papkalarni avtomatik xaritalashga harakat qilish (\"Sent items\", \"Sent\" => \"Sent\" va h.k.)", + "backup_mx_options": "Relay sozlamalari", + "bcc_dest_format": "BCC manzili bitta haqiqiy e-pochta manzili boʻlishi kerak.
    Agar bir nechta manzilga nusxa joʻnatish kerak boʻlsa, taxallus yarating va shu yerda ishlating.", + "comment_info": "Maxfiy izoh foydalanuvchiga koʻrinmaydi, ommaviy izoh esa foydalanuvchi koʻrinishida ustiga sichqoncha kelganda maslahat sifatida koʻrsatiladi", + "custom_params": "Maxsus parametrlar", + "custom_params_hint": "Toʻgʻri: --param=xy, notoʻgʻri: --param xy", + "delete1": "Tugatilgach manbadan oʻchirish", + "delete2": "Manbada boʻlmagan xabarlarni manzildan oʻchirish", + "delete2duplicates": "Manzildagi dublikatlarni oʻchirish", + "description": "Tavsif", + "destination": "Manzil", + "disable_login": "Kirishni taqiqlash (kiruvchi pochta hali ham qabul qilinadi)", + "domain": "Domen", + "domain_matches_hostname": "%s domeni xost nomiga mos keladi", + "domain_quota_m": "Domenning umumiy kvotasi (MiB)", + "dry": "Sinxronizatsiyani simulyatsiya qilish", + "enc_method": "Shifrlash usuli", + "exclude": "Obyektlarni istisno qilish (regex)", + "full_name": "Toʻliq ism", + "gal": "Global manzillar roʻyxati", + "gal_info": "GAL domenning barcha obyektlarini oʻz ichiga oladi va hech bir foydalanuvchi tomonidan tahrirlanmaydi. Oʻchirilsa, SOGodagi bandlik maʼlumotlari yoʻqoladi! Oʻzgarishlarni qoʻllash uchun SOGoni qayta ishga tushiring.", + "generate": "yaratish", + "goto_ham": "ham sifatida oʻrgatish", + "goto_null": "Xatni jimgina rad etish", + "goto_spam": "spam sifatida oʻrgatish", + "hostname": "Xost", + "inactive": "Nofaol", + "internal": "Ichki", + "internal_info": "Ichki taxalluslarga faqat oʻz domeni yoki taxallus domenlaridan kirish mumkin.", + "sender_allowed": "Ushbu taxallus orqali joʻnatishga ruxsat berish", + "kind": "Turi", + "mailbox_quota_def": "Standart pochta qutisi kvotasi", + "mailbox_quota_m": "Bitta pochta qutisining maks. kvotasi (MiB)", + "mailbox_username": "Foydalanuvchi nomi (e-pochta manzilining chap qismi)", + "max_aliases": "Mumkin boʻlgan maks. taxalluslar", + "max_mailboxes": "Mumkin boʻlgan maks. pochta qutilari", + "mins_interval": "Soʻrov oraligʻi (daqiqa)", + "multiple_bookings": "Bir nechta bandlovlar", + "nexthop": "Keyingi nuqta", + "password": "Parol", + "password_repeat": "Tasdiqlash paroli (takrorlang)", + "port": "Port", + "post_domain_add": "Yangi domen qoʻshilgandan keyin SOGo konteyneri (\"sogo-mailcow\") qayta ishga tushirilishi kerak!

    Bundan tashqari, domenning DNS sozlamalarini koʻrib chiqing. DNS sozlamalari tasdiqlangach, yangi domeningiz (autoconfig.<domain>, autodiscover.<domain>) uchun sertifikatlarni avtomatik yaratish uchun \"acme-mailcow\" konteynerini qayta ishga tushiring.
    Bu qadam ixtiyoriy va har 24 soatda qayta urinib koʻriladi.", + "private_comment": "Maxfiy izoh", + "public_comment": "Ommaviy izoh", + "quota_mb": "Kvota (MiB)", + "relay_all": "Barcha qabul qiluvchilarni relay qilish", + "relay_all_info": "↪ Agar barcha qabul qiluvchilarni relay qilmaslikni tanlasangiz, relay qilinishi kerak boʻlgan har bir qabul qiluvchi uchun (\"koʻr\") pochta qutisi qoʻshishingiz kerak boʻladi.", + "relay_domain": "Ushbu domenni relay qilish", + "relay_transport_info": "
    Maʼlumot
    Bu domen uchun maxsus manzilga transport xaritalarini belgilashingiz mumkin. Agar oʻrnatilmagan boʻlsa, MX qidiruvi amalga oshiriladi.", + "relay_unknown_only": "Faqat mavjud boʻlmagan pochta qutilarini relay qilish. Mavjud pochta qutilari mahalliy yetkazib beriladi.", + "relayhost_wrapped_tls_info": "Iltimos, TLS-oʻralgan portlardan foydalanmang (asosan 465-portda).
    \r\nO'ralmagan istalgan portdan foydalaning va STARTTLS chiqaring. TLS ni majburlash uchun TLS siyosatini \"TLS siyosati xaritalari\"da yaratish mumkin.", + "select": "Iltimos, tanlang...", + "select_domain": "Avval domenni tanlang", + "sieve_desc": "Qisqa tavsif", + "sieve_type": "Filtr turi", + "skipcrossduplicates": "Papkalararo dublikat xabarlarni oʻtkazib yuborish (birinchi kelgan, birinchi xizmatlanadi)", + "subscribeall": "Barcha papkalarga obuna boʻlish", + "syncjob": "Sinxronizatsiya vazifasi qoʻshish", + "syncjob_hint": "Parollar oddiy matn sifatida saqlanishini unutmang!", + "tags": "Teglar", + "target_address": "Yoʻnaltirish manzillari", + "target_address_info": "Toʻliq e-pochta manzil(lar)i (vergul bilan ajratilgan).", + "target_domain": "Maqsad domen", + "timeout1": "Masofaviy xostga ulanish vaqtining tugashi", + "timeout2": "Mahalliy xostga ulanish vaqtining tugashi", + "username": "Foydalanuvchi nomi", + "validate": "Tekshirish", + "validation_success": "Muvaffaqiyatli tekshirildi" + }, + "admin": { + "access": "Kirish", + "action": "Amal", + "activate_api": "API ni faollashtirish", + "activate_send": "Joʻnatish tugmasini faollashtirish", + "active": "Faol", + "active_rspamd_settings_map": "Faol sozlamalar xaritasi", + "add": "Qoʻshish", + "add_admin": "Administrator qoʻshish", + "add_domain_admin": "Domen administratorini qoʻshish", + "add_forwarding_host": "Yoʻnaltirish xosti qoʻshish", + "add_relayhost": "Joʻnatuvchiga bogʻliq transport qoʻshish", + "add_relayhost_hint": "Autentifikatsiya maʼlumotlari, agar boʻlsa, oddiy matn sifatida saqlanishini unutmang.", + "add_row": "Satr qoʻshish", + "add_settings_rule": "Sozlamalar qoidasini qoʻshish", + "add_transport": "Transport qoʻshish", + "add_transports_hint": "Autentifikatsiya maʼlumotlari, agar boʻlsa, oddiy matn sifatida saqlanishini unutmang.", + "additional_rows": " ta qoʻshimcha satr qoʻshildi", + "admin": "Administrator", + "admin_details": "Administrator maʼlumotlarini tahrirlash", + "admin_domains": "Domen tayinlashlari", + "admins": "Administratorlar", + "admins_ldap": "LDAP administratorlari", + "admin_quicklink": "Admin kirish sahifasiga tezkor havolani yashirish", + "advanced_settings": "Kengaytirilgan sozlamalar", + "allowed_methods": "Access-Control-Allow-Methods", + "allowed_origins": "Access-Control-Allow-Origin", + "api_allow_from": "API kirishini ushbu IP / CIDR tarmoq belgilanishlaridan ruxsat berish", + "api_info": "API ishlab chiqilmoqda. Hujjatlarni /api manzilida topishingiz mumkin", + "api_key": "API kaliti", + "api_read_only": "Faqat oʻqish uchun kirish", + "api_read_write": "Oʻqish va yozish uchun kirish", + "api_skip_ip_check": "API uchun IP tekshiruvini oʻtkazib yuborish", + "app_hide": "Kirishda yashirish", + "app_links": "Ilova havolalari", + "app_name": "Ilova nomi", + "apps_name": "\"mailcow Apps\" nomi", + "arrival_time": "Kelish vaqti (server vaqti)", + "authed_user": "Avtorizatsiya qilingan foydalanuvchi", + "ays": "Davom etishni xohlayotganingizga ishonchingiz komilmi?", + "ban_list_info": "Quyida bloklangan IP roʻyxati keltirilgan: tarmoq (qolgan blok vaqti) - [amallar].
    Blokdan chiqarishga navbatga qoʻyilgan IP lar bir necha soniya ichida faol bloklash roʻyxatidan olib tashlanadi.
    Qizil yorliqlar qora roʻyxat orqali faol doimiy bloklarni bildiradi.", + "change_logo": "Logotipni oʻzgartirish", + "logo_normal_label": "Oddiy", + "logo_dark_label": "Qorongʻi rejim uchun teskari", + "configuration": "Konfiguratsiya", + "convert_html_to_text": "HTMLni oddiy matnga aylantirish", + "copy_to_clipboard": "Matn vaqtinchalik xotiraga koʻchirildi!", + "cors_settings": "CORS sozlamalari", + "credentials_transport_warning": "Diqqat: Yangi transport xaritasi yozuvi qoʻshish keyingi nuqta ustunlari mos keladigan barcha yozuvlar uchun maʼlumotlarni yangilaydi.", + "customer_id": "Mijoz ID", + "customize": "Sozlash", + "login_page": "Kirish sahifasi", + "destination": "Manzil", + "dkim_add_key": "ARC/DKIM kaliti qoʻshish", + "dkim_domains_selector": "Tanlovchi", + "dkim_domains_wo_keys": "Kalit yetishmaydigan domenlarni tanlash", + "dkim_from": "Kimdan", + "dkim_from_title": "Maʼlumotlarni koʻchirib olish uchun manba domen", + "dkim_key_length": "DKIM kalit uzunligi (bit)", + "dkim_key_missing": "Kalit yetishmaydi", + "dkim_key_unused": "Kalit ishlatilmagan", + "dkim_key_valid": "Kalit haqiqiy", + "dkim_keys": "ARC/DKIM kalitlari", + "dkim_overwrite_key": "Mavjud DKIM kalitini ustidan yozish", + "dkim_private_key": "Maxfiy kalit", + "dkim_to": "Kimga", + "dkim_to_title": "Maqsad domen(lar) — ustidan yoziladi", + "domain": "Domen", + "domain_admin": "Domen administratori", + "domain_admins": "Domen administratorlari", + "domainadmin_quicklink": "Domen admin kirish sahifasiga tezkor havolani yashirish", + "domain_s": "Domen(lar)", + "duplicate": "Nusxalash", + "duplicate_dkim": "DKIM yozuvini nusxalash", + "edit": "Tahrirlash", + "empty": "Natijalar yoʻq", + "excludes": "Ushbu qabul qiluvchilarni istisno qiladi", + "f2b_ban_time": "Bloklash vaqti (s)", + "f2b_ban_time_increment": "Har bir bloklashda blok vaqti ortadi", + "f2b_blacklist": "Qora roʻyxatdagi tarmoqlar/xostlar", + "f2b_filter": "Regex filtrlari", + "f2b_list_info": "Qora roʻyxatdagi xost yoki tarmoq har doim oq roʻyxat elementidan ustun keladi. Roʻyxat yangilanishlarini qoʻllash uchun bir necha soniya kerak boʻladi.", + "f2b_manage_external": "Fail2ban ni tashqi boshqarish", + "f2b_manage_external_info": "Fail2ban hali ham banlist ni saqlaydi, ammo trafikni bloklash uchun faol qoidalarni oʻrnatmaydi. Trafikni tashqi tomondan bloklash uchun quyida yaratilgan banlist dan foydalaning.", + "f2b_max_attempts": "Maks. urinishlar", + "f2b_max_ban_time": "Maks. bloklash vaqti (s)", + "f2b_netban_ipv4": "Blokni qoʻllash uchun IPv4 subtarmoq oʻlchami (8-32)", + "f2b_netban_ipv6": "Blokni qoʻllash uchun IPv6 subtarmoq oʻlchami (8-128)", + "f2b_parameters": "Fail2ban parametrlari", + "f2b_regex_info": "Eʼtiborga olinadigan jurnallar: SOGo, Postfix, Dovecot, PHP-FPM.", + "f2b_retry_window": "Maks. urinishlar uchun qayta urinish oynasi (s)", + "f2b_whitelist": "Oq roʻyxatdagi tarmoqlar/xostlar", + "filter": "Filtr", + "filter_table": "Filtr jadvali", + "force_sso_text": "Tashqi OIDC provayder sozlangan boʻlsa, bu variant standart mailcow kirish formalarini yashiradi va faqat Yagona kirish tugmasini koʻrsatadi", + "force_sso": "mailcow kirishini oʻchirib, faqat Yagona kirishni koʻrsatish", + "forwarding_hosts": "Yoʻnaltirish xostlari", + "forwarding_hosts_add_hint": "Siz IPv4/IPv6 manzillarini, CIDR yozuvidagi tarmoqlarni, xost nomlarini (IP manzillariga aylantiriladi) yoki domen nomlarini (SPF TXT yoki yoʻq boʻlsa MX yozuvlarini soʻrash orqali IP manzillariga aylantiriladi) belgilashingiz mumkin.", + "forwarding_hosts_hint": "Kiruvchi xabarlar bu yerda keltirilgan istalgan xostlardan shartsiz qabul qilinadi. Bu xostlar keyin DNSBL ga qarshi tekshirilmaydi yoki greylisting ga olinmaydi. Ulardan kelgan spam hech qachon rad etilmaydi, ammo ixtiyoriy ravishda Junk papkasiga joylashtirilishi mumkin. Buning eng keng tarqalgan ishlatilishi — mailcow serveringizga kiruvchi xatlarni yoʻnaltirish qoidasi oʻrnatilgan pochta serverlarini koʻrsatish.", + "from": "Kimdan", + "generate": "yaratish", + "guid": "GUID - noyob nusxa ID si", + "guid_and_license": "GUID va litsenziya", + "hash_remove_info": "Ratelimit hashini oʻchirish (agar mavjud boʻlsa) uning hisoblagichini butunlay tiklaydi.
    \r\n Har bir hash alohida rang bilan koʻrsatiladi.", + "help_text": "Kirish maskasi ostidagi yordam matnini bekor qilish (HTML ruxsat etilgan)", + "host": "Xost", + "html": "HTML", + "iam": "Identifikatsiya provayderi", + "iam_attribute_field": "Atribut maydoni", + "iam_authorize_url": "Avtorizatsiya nuqtasi", + "iam_auth_flow": "Autentifikatsiya oqimi", + "iam_auth_flow_info": "Yagona kirish (SSO) uchun ishlatiladigan Authorization Code Flow (Keycloak da Standard Flow) qoʻshimcha ravishda, mailcow toʻgʻridan-toʻgʻri maʼlumotlar bilan Autentifikatsiya oqimini ham qoʻllab-quvvatlaydi. Mailpassword Flow Keycloak Admin REST API yordamida foydalanuvchi maʼlumotlarini tekshirishga harakat qiladi. mailcow Keycloakda xaritalangan mailcow_password atributidan xeshlangan parolni oladi.", + "iam_basedn": "Base DN", + "iam_client_id": "Mijoz ID", + "iam_client_secret": "Mijoz maxfiy soʻzi", + "iam_client_scopes": "Mijoz qamrovlari", + "iam_default_template": "Standart shablon", + "iam_default_template_description": "Agar foydalanuvchiga shablon tayinlanmagan boʻlsa, pochta qutisini yaratishda standart shablon ishlatiladi, lekin pochta qutisini yangilash uchun emas.", + "iam_description": "Autentifikatsiya uchun tashqi provayderni sozlash
    Foydalanuvchi pochta qutilari atribut xaritalashi oʻrnatilgan boʻlsa, ular birinchi marta kirgan zahoti avtomatik yaratiladi.", + "iam_extra_permission": "Quyidagi sozlamalarning ishlashi uchun Keycloak dagi mailcow mijoziga Service account va view-users ruxsati kerak.", + "iam_host": "Xost", + "iam_host_info": "Bir yoki bir nechta LDAP xostni vergul bilan ajratib kiriting.", + "iam_import_users": "Foydalanuvchilarni import qilish", + "iam_login_provisioning": "Kirishda foydalanuvchilarni avto-yaratish", + "iam_mapping": "Atribut xaritalashi", + "iam_bindpass": "Bind paroli", + "iam_periodic_full_sync": "Davriy toʻliq sinxronizatsiya", + "iam_port": "Port", + "iam_realm": "Realm", + "iam_redirect_url": "Yoʻnaltirish URL", + "iam_rest_flow": "Mailpassword Flow", + "iam_server_url": "Server URL", + "iam_sso": "Yagona kirish (SSO)", + "iam_sync_interval": "Sinxronizatsiya / import oraligʻi (daqiqa)", + "iam_test_connection": "Ulanishni tekshirish", + "iam_token_url": "Token nuqtasi", + "iam_userinfo_url": "Foydalanuvchi maʼlumotlari nuqtasi", + "iam_username_field": "Foydalanuvchi nomi maydoni", + "iam_binddn": "Bind DN", + "iam_use_ssl": "SSL dan foydalanish", + "iam_use_ssl_info": "SSL yoqilsa va port 389 ga oʻrnatilgan boʻlsa, u avtomatik ravishda 636 ga oʻzgartiriladi.", + "iam_use_tls": "StartTLS dan foydalanish", + "iam_use_tls_info": "TLS yoqilsa, LDAP serveringiz uchun standart portni (389) ishlatishingiz kerak. SSL portlari ishlatilmaydi.", + "iam_version": "Versiya", + "ignore_ssl_error": "SSL xatolarini eʼtiborsiz qoldirish", + "import": "Import", + "import_private_key": "Maxfiy kalitni import qilish", + "in_use_by": "Kim ishlatadi", + "inactive": "Nofaol", + "include_exclude": "Kiritish/Istisno qilish", + "include_exclude_info": "Standart holatda — tanlovsiz — barcha pochta qutilariga murojaat qilinadi", + "includes": "Ushbu qabul qiluvchilarni kiritish", + "ip_check": "IP tekshiruvi", + "ip_check_disabled": "IP tekshiruvi oʻchirilgan. Uni quyidagi joydan yoqishingiz mumkin:
    Tizim > Konfiguratsiya > Variantlar > Sozlash", + "ip_check_opt_in": "Tashqi IP manzillarni aniqlash uchun ipv4.mailcow.email va ipv6.mailcow.email uchinchi tomon xizmatidan foydalanishga rozilik.", + "is_mx_based": "MX asosida", + "last_applied": "Oxirgi qoʻllanilgan", + "license_info": "Litsenziya talab qilinmaydi, ammo bu loyihaning rivojlanishiga yordam beradi.
    GUID ni bu yerda roʻyxatdan oʻtkazing yoki mailcow oʻrnatmangiz uchun yordam sotib oling.", + "link": "Havola", + "loading": "Iltimos, kuting...", + "login_time": "Kirish vaqti", + "logo_info": "Tasviringiz yuqori navigatsiya paneli uchun 40px balandlikka, bosh sahifa uchun maks. 250px kenglikka oʻlchanadi. Masshtablanadigan grafika tavsiya etiladi.", + "lookup_mx": "Manzil — MX nomiga mos keladigan muntazam ifoda (.*\\.google\\.com — google.com bilan tugaydigan MX ga yoʻnaltirilgan barcha pochtani ushbu nuqta orqali yoʻnaltirish uchun)", + "main_name": "\"mailcow UI\" nomi", + "merged_vars_hint": "Kulrang satrlar vars.(local.)inc.php dan birlashtirilgan va oʻzgartirib boʻlmaydi.", + "message": "Xabar", + "message_size": "Xabar oʻlchami", + "nexthop": "Keyingi nuqta", + "needs_restart": "qayta ishga tushirish kerak", + "no": "✕", + "no_active_bans": "Faol bloklar yoʻq", + "no_new_rows": "Boshqa satrlar mavjud emas", + "no_record": "Yozuv yoʻq", + "oauth2_apps": "OAuth2 ilovalari", + "oauth2_add_client": "OAuth2 mijozini qoʻshish", + "oauth2_client_id": "Mijoz ID", + "oauth2_client_secret": "Mijoz maxfiy soʻzi", + "oauth2_info": "OAuth2 amalga oshirilishi \"Authorization Code\" grant turini qoʻllab-quvvatlaydi va refresh tokenlarini chiqaradi.
    \r\nServer shuningdek refresh token ishlatilgandan keyin avtomatik ravishda yangi refresh tokenlarini chiqaradi.

    \r\n• Standart qamrov — profile. Faqat pochta qutisi foydalanuvchilari OAuth2 ga qarshi autentifikatsiya qilinishi mumkin. Agar scope parametri tashlab ketilgan boʻlsa, u profile ga qaytadi.
    \r\n• state parametri mijoz tomonidan avtorizatsiya soʻrovining bir qismi sifatida yuborilishi shart.

    \r\nOAuth2 API ga soʻrovlar uchun yoʻllar:
    \r\n
      \r\n
    • Avtorizatsiya nuqtasi: /oauth/authorize
    • \r\n
    • Token nuqtasi: /oauth/token
    • \r\n
    • Resurs sahifasi: /oauth/profile
    • \r\n
    \r\nMijoz maxfiy soʻzini qayta yaratish mavjud avtorizatsiya kodlarini muddati tugashiga olib kelmaydi, lekin ular tokenni yangilashda muvaffaqiyatsiz boʻladi.

    \r\nMijoz tokenlarini bekor qilish barcha faol sessiyalarning darhol tugashiga olib keladi. Barcha mijozlar qayta autentifikatsiya qilishlari kerak.", + "oauth2_redirect_uri": "Yoʻnaltirish URI", + "oauth2_renew_secret": "Yangi mijoz maxfiy soʻzini yaratish", + "oauth2_revoke_tokens": "Barcha mijoz tokenlarini bekor qilish", + "optional": "ixtiyoriy", + "options": "Variantlar", + "password": "Parol", + "password_length": "Parol uzunligi", + "password_policy": "Parol siyosati", + "password_policy_chars": "Kamida bitta alifbo belgisini oʻz ichiga olishi kerak", + "password_policy_length": "Minimal parol uzunligi %d", + "password_policy_lowerupper": "Kichik va katta harflarni oʻz ichiga olishi kerak", + "password_policy_numbers": "Kamida bitta raqamni oʻz ichiga olishi kerak", + "password_policy_special_chars": "Maxsus belgilarni oʻz ichiga olishi kerak", + "password_repeat": "Tasdiqlash paroli (takrorlang)", + "password_reset_info": "Agar tiklash uchun e-pochta kiritilmagan boʻlsa, bu funksiyadan foydalanib boʻlmaydi.", + "password_reset_settings": "Parolni tiklash sozlamalari", + "password_reset_tmpl_html": "HTML shablon", + "password_reset_tmpl_text": "Matnli shablon", + "password_settings": "Parol sozlamalari", + "priority": "Muhimlik darajasi", + "private_key": "Maxfiy kalit", + "quarantine": "Karantin", + "quarantine_bcc": "Barcha bildirishnomalarning nusxasini (BCC) ushbu qabul qiluvchiga yuborish:
    Oʻchirish uchun boʻsh qoldiring. Imzolanmagan, tekshirilmagan xat. Faqat ichki yetkazib berilishi kerak.", + "quarantine_exclude_domains": "Domenlar va taxallus domenlarini istisno qilish", + "quarantine_max_age": "Maksimal yosh (kunlarda)
    Qiymat 1 kun yoki undan koʻp boʻlishi kerak.", + "quarantine_max_score": "Agar xatning spam bahosi ushbu qiymatdan yuqori boʻlsa, bildirishnomani rad etish:
    Standart 9999.0", + "quarantine_max_size": "Maksimal oʻlcham (MiB) (kattaroq elementlar rad etiladi):
    0 cheksizlikni bildirmaydi.", + "quarantine_notification_html": "Bildirishnoma e-pochta shabloni:
    Standart shablonni tiklash uchun boʻsh qoldiring.", + "quarantine_notification_sender": "Bildirishnoma e-pochta joʻnatuvchisi", + "quarantine_notification_subject": "Bildirishnoma e-pochta mavzusi", + "quarantine_redirect": "Barcha bildirishnomalarni ushbu qabul qiluvchiga yoʻnaltirish:
    Oʻchirish uchun boʻsh qoldiring. Imzolanmagan, tekshirilmagan xat. Faqat ichki yetkazib berilishi kerak.", + "quarantine_release_format": "Chiqarilgan elementlar formati", + "quarantine_release_format_att": "Ilova sifatida", + "quarantine_release_format_raw": "Oʻzgartirilmagan asl nusxa", + "quarantine_retention_size": "Pochta qutisi uchun saqlash:
    0 — nofaolligini bildiradi.", + "quicklink_text": "Boshqa kirish sahifalariga tezkor havolalarni kirish formasi ostida koʻrsatish yoki yashirish", + "quota_notification_html": "Bildirishnoma e-pochta shabloni:
    Standart shablonni tiklash uchun boʻsh qoldiring.", + "quota_notification_sender": "Bildirishnoma e-pochta joʻnatuvchisi", + "quota_notification_subject": "Bildirishnoma e-pochta mavzusi", + "quota_notifications": "Kvota bildirishnomalari", + "quota_notifications_info": "Kvota bildirishnomalari foydalanuvchilarga foydalanish 80% va 95% ga yetganda bir martadan yuboriladi.", + "quota_notifications_vars": "{{percent}} — foydalanuvchining joriy kvotasi
    {{username}} — pochta qutisi nomi", + "queue_unban": "blokdan chiqarish", + "r_active": "Faol cheklovlar", + "r_inactive": "Nofaol cheklovlar", + "r_info": "Faol cheklovlar roʻyxatidagi kulrang/oʻchirilgan elementlar mailcow uchun haqiqiy cheklovlar sifatida tanilmaydi va koʻchirib boʻlmaydi. Nomaʼlum cheklovlar baribir koʻrinish tartibida oʻrnatiladi.
    inc/vars.local.inc.php da yangi elementlarni qoʻshib, ularni almashtirishingiz mumkin.", + "rate_name": "Tezlik nomi", + "recipients": "Qabul qiluvchilar", + "refresh": "Yangilash", + "regen_api_key": "API kalitini qayta yaratish", + "regex_maps": "Regex xaritalari", + "relay_from": "\"From:\" manzili", + "relay_rcpt": "\"To:\" manzili", + "relay_run": "Sinovni ishga tushirish", + "relayhosts": "Joʻnatuvchiga bogʻliq transportlar", + "relayhosts_hint": "Domen konfiguratsiya muloqotida tanlash mumkin boʻlgan joʻnatuvchiga bogʻliq transportlarni belgilang.
    \r\n Transport xizmati har doim \"smtp:\" va shuning uchun taklif qilinganda TLS ga harakat qiladi. Oʻralgan TLS (SMTPS) qoʻllab-quvvatlanmaydi. Foydalanuvchining individual chiquvchi TLS siyosati sozlamasi eʼtiborga olinadi.
    \r\n Taxallus domenlari bilan birga tanlangan domenlarga taʼsir qiladi.", + "remove": "Olib tashlash", + "remove_row": "Satrni olib tashlash", + "reset_default": "Standartga qaytarish", + "reset_limit": "Hash ni olib tashlash", + "reset_password_vars": "{{link}} Yaratilgan parolni tiklash havolasi
    {{username}} Parolni tiklashni soʻragan foydalanuvchining pochta qutisi nomi
    {{username2}} Tiklash uchun pochta qutisi nomi
    {{date}} Parolni tiklash soʻrovi qilingan sana
    {{token_lifetime}} Daqiqalardagi token muddati
    {{hostname}} mailcow xost nomi", + "restore_template": "Standart shablonni tiklash uchun boʻsh qoldiring.", + "routing": "Yoʻnaltirish", + "rsetting_add_rule": "Qoida qoʻshish", + "rsetting_content": "Qoida mazmuni", + "rsetting_desc": "Qisqa tavsif", + "rsetting_no_selection": "Iltimos, qoidani tanlang", + "rsetting_none": "Qoidalar mavjud emas", + "rsettings_insert_preset": "Misol presetini kiritish \"%s\"", + "rsettings_preset_1": "Autentifikatsiya qilingan foydalanuvchilar uchun DKIM va tezlik chegarasidan boshqa hamma narsani oʻchirib qoʻyish", + "rsettings_preset_2": "Postmasterlar spamni xohlaydi", + "rsettings_preset_3": "Pochta qutisi uchun faqat maxsus joʻnatuvchilarga ruxsat berish (yaʼni, faqat ichki pochta qutisi sifatida ishlatish)", + "rsettings_preset_4": "Domen uchun Rspamd ni oʻchirish", + "rspamd_com_settings": "Sozlama nomi avtomatik yaratiladi, quyidagi misol presetlariga qarang. Batafsil maʼlumot uchun Rspamd hujjatlariga qarang", + "rspamd_global_filters": "Global filtr xaritalari", + "rspamd_global_filters_agree": "Ehtiyot boʻlaman!", + "rspamd_global_filters_info": "Global filtr xaritalari turli xil global rad etish va ruxsat berish roʻyxatlarini oʻz ichiga oladi.", + "rspamd_global_filters_regex": "Ularning nomlari ularning maqsadini tushuntiradi. Barcha mazmun \"/pattern/options\" formatidagi haqiqiy muntazam ifodalarni oʻz ichiga olishi kerak (masalan, /.+@domain\\.tld/i).
    \r\n Har bir regex satriga oddiy tekshiruvlar bajariladi, ammo agar Rspamd sintaksisni toʻgʻri oʻqiy olmasa, uning funksionalligi buzilishi mumkin.
    \r\n Rspamd oʻzgarganda xarita mazmunini oʻqishga harakat qiladi. Agar muammolarga duch kelsangiz, xarita qayta yuklanishi uchun Rspamd ni qayta ishga tushiring.
    Qora roʻyxatdagi elementlar karantindan istisno qilinadi.", + "rspamd_settings_map": "Rspamd sozlamalar xaritasi", + "sal_level": "Moo darajasi", + "save": "Oʻzgarishlarni saqlash", + "search_domain_da": "Domenlarni qidirish", + "send": "Joʻnatish", + "sender": "Joʻnatuvchi", + "service": "Xizmat", + "service_id": "Xizmat ID", + "source": "Manba", + "spamfilter": "Spam filtri", + "subject": "Mavzu", + "success": "Muvaffaqiyatli", + "sys_mails": "Tizim xatlari", + "task": "Vazifa", + "text": "Matn", + "time": "Vaqt", + "title": "Sarlavha", + "title_name": "\"mailcow UI\" veb-sayt sarlavhasi", + "to_top": "Yuqoriga qaytish", + "transport_dest_format": "Regex yoki sintaksis: example.org, .example.org, *, box@example.org (bir nechta qiymatlarni vergul bilan ajratish mumkin)", + "transport_maps": "Transport xaritalari", + "transport_test_rcpt_info": "• Tashqi manzilga relay qilishni sinash uchun null@hosted.mailcow.de dan foydalaning.", + "transports_hint": "• Transport xaritasi yozuvi joʻnatuvchiga bogʻliq transport xaritasidan ustun.
    \r\n• MX asosidagi transportlar afzalroq ishlatiladi.
    \r\n• Foydalanuvchi boʻyicha chiquvchi TLS siyosati sozlamalari eʼtiborga olinmaydi va faqat TLS siyosati xaritasi yozuvlari orqali majburlanishi mumkin.
    \r\n• Belgilangan transportlar uchun transport xizmati har doim \"smtp:\" va shuning uchun taklif qilinganda TLS ga harakat qiladi. Oʻralgan TLS (SMTPS) qoʻllab-quvvatlanmaydi.
    \r\n• \"/localhost$/\" ga mos keladigan manzillar har doim \"local:\" orqali tashiladi, shuning uchun \"*\" manzili bu manzillarga qoʻllanmaydi.
    \r\n• Misol uchun keyingi nuqta \"[host]:25\" uchun maʼlumotlarni aniqlash uchun, Postfix har doim \"[host]:25\" ni qidirishdan oldin \"host\" ni soʻraydi. Bu xatti-harakat \"host\" va \"[host]:25\" ni bir vaqtning oʻzida ishlatishni imkonsiz qiladi.", + "ui_footer": "Pastki qism (HTML ruxsat etilgan)", + "ui_header_announcement": "Eʼlonlar", + "ui_header_announcement_active": "Eʼlonni faollashtirish", + "ui_header_announcement_content": "Matn (HTML ruxsat etilgan)", + "ui_header_announcement_help": "Eʼlon barcha tizimga kirgan foydalanuvchilarga va UI ning kirish ekranida koʻrinadi.", + "ui_header_announcement_select": "Eʼlon turini tanlang", + "ui_header_announcement_type": "Tur", + "ui_header_announcement_type_danger": "Juda muhim", + "ui_header_announcement_type_info": "Maʼlumot", + "ui_header_announcement_type_warning": "Muhim", + "ui_texts": "UI yorliqlari va matnlari", + "unban_pending": "blokdan chiqarish kutilmoqda", + "unchanged_if_empty": "Oʻzgartirilmagan boʻlsa, boʻsh qoldiring", + "upload": "Yuklash", + "username": "Foydalanuvchi nomi", + "user_link": "Foydalanuvchi havolasi", + "user_quicklink": "Foydalanuvchi kirish sahifasiga tezkor havolani yashirish", + "validate_license_now": "GUID ni litsenziya serveriga qarshi tekshirish", + "verify": "Tasdiqlash", + "yes": "✓" + }, + "danger": { + "access_denied": "Kirish rad etildi yoki noma'lum forma ma'lumotlari", + "tfa_removal_blocked": "Ikki faktorli autentifikatsiyani olib tashlab boʻlmaydi, chunki u sizning hisobingiz uchun talab qilinadi.", + "alias_domain_invalid": "%s taxallus domeni noto'g'ri", + "alias_empty": "Taxallus manzili boʻsh boʻlmasligi kerak", + "alias_goto_identical": "Taxallus va yoʻnaltirish manzili bir xil boʻlmasligi kerak", + "alias_invalid": "%s taxallus manzili noto'g'ri", + "aliasd_targetd_identical": "Taxallus domeni maqsad domeniga teng boʻlmasligi kerak: %s", + "aliases_in_use": "Maks. taxalluslar %d dan katta yoki teng boʻlishi kerak", + "app_name_empty": "Ilova nomi boʻsh boʻlolmaydi", + "app_passwd_id_invalid": "Ilova paroli ID %s noto'g'ri", + "authsource_in_use": "Identifikatsiya provayderini oʻzgartirib yoki oʻchirib boʻlmaydi, chunki u hozirda bir yoki bir nechta foydalanuvchilar tomonidan ishlatilmoqda.", + "bcc_empty": "BCC manzili boʻsh boʻlolmaydi", + "bcc_exists": "%s turi uchun %s BCC xaritasi mavjud", + "bcc_must_be_email": "BCC manzili %s haqiqiy e-pochta manzili emas", + "comment_too_long": "Izoh juda uzun, maks. 160 ta belgiga ruxsat berilgan", + "cors_invalid_method": "Noto'g'ri Allow-Method belgilangan", + "cors_invalid_origin": "Noto'g'ri Allow-Origin belgilangan", + "defquota_empty": "Bitta pochta qutisi uchun standart kvota 0 boʻlmasligi kerak.", + "demo_mode_enabled": "Demo rejim yoqilgan", + "description_invalid": "%s uchun resurs tavsifi noto'g'ri", + "dkim_domain_or_sel_exists": "\"%s\" uchun DKIM kaliti mavjud va u ustidan yozilmaydi", + "dkim_domain_or_sel_invalid": "DKIM domeni yoki tanlovchi noto'g'ri: %s", + "domain_cannot_match_hostname": "Domen xost nomiga mos kelishi mumkin emas", + "domain_exists": "%s domeni allaqachon mavjud", + "domain_invalid": "Domen nomi boʻsh yoki noto'g'ri", + "domain_not_empty": "Boʻsh boʻlmagan %s domenini olib tashlab boʻlmaydi", + "domain_not_found": "%s domeni topilmadi", + "domain_quota_m_in_use": "Domen kvotasi %s MiB dan katta yoki teng boʻlishi kerak", + "extended_sender_acl_denied": "Tashqi joʻnatuvchi manzillarini oʻrnatish uchun ACL yetishmayapti", + "extra_acl_invalid": "Tashqi joʻnatuvchi manzili \"%s\" noto'g'ri", + "extra_acl_invalid_domain": "Tashqi joʻnatuvchi \"%s\" noto'g'ri domendan foydalanmoqda", + "fido2_verification_failed": "FIDO2 tekshiruvi muvaffaqiyatsiz: %s", + "file_open_error": "Faylni yozish uchun ochib boʻlmadi", + "filter_type": "Notoʻgʻri filtr turi", + "from_invalid": "Joʻnatuvchi boʻsh boʻlmasligi kerak", + "generic_server_error": "Kutilmagan server xatosi yuz berdi. Iltimos, administratoringiz bilan bogʻlaning.", + "global_filter_write_error": "Filtr faylini yozib boʻlmadi: %s", + "global_map_invalid": "Global xarita ID %s noto'g'ri", + "global_map_write_error": "Global xarita ID %s ni yozib boʻlmadi: %s", + "goto_empty": "Taxallus manzili kamida bitta haqiqiy yoʻnaltirish manzilini oʻz ichiga olishi kerak", + "goto_invalid": "Yoʻnaltirish manzili %s noto'g'ri", + "ham_learn_error": "Ham oʻrgatish xatosi: %s", + "iam_test_connection": "Ulanish muvaffaqiyatsiz", + "imagick_exception": "Xato: Tasvirni oʻqishda Imagick istisnosi", + "img_dimensions_exceeded": "Tasvir maksimal tasvir oʻlchamidan oshib ketdi", + "img_invalid": "Tasvir faylini tekshirib boʻlmadi", + "img_size_exceeded": "Tasvir maksimal fayl oʻlchamidan oshib ketdi", + "img_tmp_missing": "Tasvir faylini tekshirib boʻlmadi: Vaqtinchalik fayl topilmadi", + "invalid_bcc_map_type": "Noto'g'ri BCC xarita turi", + "invalid_destination": "Manzil formati \"%s\" noto'g'ri", + "invalid_filter_type": "Noto'g'ri filtr turi", + "invalid_host": "Noto'g'ri xost belgilangan: %s", + "invalid_mime_type": "Noto'g'ri MIME turi", + "invalid_nexthop": "Keyingi nuqta formati noto'g'ri", + "invalid_nexthop_authenticated": "Keyingi nuqta boshqa maʼlumotlar bilan mavjud, iltimos avval bu keyingi nuqta uchun mavjud maʼlumotlarni yangilang.", + "invalid_recipient_map_new": "Yangi qabul qiluvchi noto'g'ri belgilangan: %s", + "invalid_recipient_map_old": "Asl qabul qiluvchi noto'g'ri belgilangan: %s", + "invalid_reset_token": "Noto'g'ri tiklash tokeni", + "ip_list_empty": "Ruxsat etilgan IP roʻyxati boʻsh boʻlolmaydi", + "is_alias": "%s allaqachon taxallus manzili sifatida tanilgan", + "is_alias_or_mailbox": "%s allaqachon taxallus, pochta qutisi yoki taxallus domenidan kengaytirilgan taxallus manzili sifatida tanilgan.", + "is_spam_alias": "%s allaqachon vaqtinchalik taxallus manzili (spam taxallus manzili) sifatida tanilgan", + "last_key": "Oxirgi kalitni oʻchirib boʻlmaydi, iltimos uning oʻrniga TFA ni oʻchiring.", + "login_failed": "Kirish muvaffaqiyatsiz", + "mailbox_defquota_exceeds_mailbox_maxquota": "Standart kvota maksimal kvota chegarasidan oshib ketdi", + "mailbox_invalid": "Pochta qutisi nomi noto'g'ri", + "mailbox_quota_exceeded": "Kvota domen chegarasidan oshib ketdi (maks. %d MiB)", + "mailbox_quota_exceeds_domain_quota": "Maks. kvota domen kvota chegarasidan oshib ketdi", + "mailbox_quota_left_exceeded": "Yetarli joy qolmadi (qolgan joy: %d MiB)", + "mailboxes_in_use": "Maks. pochta qutilari %d dan katta yoki teng boʻlishi kerak", + "malformed_username": "Noto'g'ri shakldagi foydalanuvchi nomi", + "map_content_empty": "Xarita mazmuni boʻsh boʻlolmaydi", + "max_age_invalid": "Maks. yosh %s noto'g'ri", + "max_alias_exceeded": "Maks. taxalluslar oshib ketdi", + "max_mailbox_exceeded": "Maks. pochta qutilari oshib ketdi (%d / %d)", + "max_quota_in_use": "Pochta qutisi kvotasi %d MiB dan katta yoki teng boʻlishi kerak", + "maxquota_empty": "Bitta pochta qutisi uchun maks. kvota 0 boʻlmasligi kerak.", + "mode_invalid": "Rejim %s noto'g'ri", + "mx_invalid": "MX yozuvi %s noto'g'ri", + "mysql_error": "MySQL xatosi: %s", + "network_host_invalid": "Noto'g'ri tarmoq yoki xost: %s", + "next_hop_interferes": "%s keyingi nuqta %s bilan tortishuvga kirishadi", + "next_hop_interferes_any": "Mavjud keyingi nuqta %s bilan tortishuvga kirishadi", + "nginx_reload_failed": "Nginx qayta yuklash muvaffaqiyatsiz: %s", + "no_user_defined": "Foydalanuvchi belgilanmagan", + "object_exists": "%s obyekti allaqachon mavjud", + "object_is_not_numeric": "%s qiymati raqamli emas", + "password_complexity": "Parol siyosatga mos kelmaydi", + "password_empty": "Parol boʻsh boʻlmasligi kerak", + "password_mismatch": "Tasdiqlash paroli mos kelmadi", + "password_reset_invalid_user": "Pochta qutisi topilmadi yoki tiklash uchun e-pochta oʻrnatilmagan", + "password_reset_na": "Parolni tiklash hozirda mavjud emas. Iltimos, administratoringiz bilan bogʻlaning.", + "policy_list_from_exists": "Berilgan nomli yozuv mavjud", + "policy_list_from_invalid": "Yozuv noto'g'ri formatga ega", + "private_key_error": "Maxfiy kalit xatosi: %s", + "pushover_credentials_missing": "Pushover tokeni va/yoki kaliti yetishmayapti", + "pushover_key": "Pushover kaliti notoʻgʻri formatga ega", + "pushover_token": "Pushover tokeni notoʻgʻri formatga ega", + "quarantine_category_invalid": "Karantin turkumi quyidagilardan biri boʻlishi kerak: add_header, reject, all", + "quota_not_0_not_numeric": "Kvota raqamli va >= 0 boʻlishi kerak", + "recipient_map_entry_exists": "Qabul qiluvchi xaritasi yozuvi \"%s\" mavjud", + "recovery_email_failed": "Tiklash e-pochtasini yuborib boʻlmadi. Iltimos, administratoringiz bilan bogʻlaning.", + "redis_error": "Redis xatosi: %s", + "relayhost_invalid": "Xarita yozuvi %s noto'g'ri", + "release_send_failed": "Xabarni chiqarib boʻlmadi: %s", + "required_data_missing": "Talab qilinadigan ma'lumotlar %s yetishmayapti", + "reset_f2b_regex": "Regex filtrini oʻz vaqtida tiklab boʻlmadi, iltimos qaytadan urinib koʻring yoki bir necha soniya kuting va sahifani qayta yuklang.", + "reset_token_limit_exceeded": "Tiklash tokeni chegarasi oshib ketdi. Iltimos, keyinroq urinib koʻring.", + "resource_invalid": "Resurs nomi %s noto'g'ri", + "rl_timeframe": "Tezlik chegarasi vaqt oraligʻi noto'g'ri", + "rspamd_ui_pw_length": "Rspamd UI paroli kamida 6 ta belgi uzunlikda boʻlishi kerak", + "script_empty": "Skript boʻsh boʻlolmaydi", + "sender_acl_invalid": "Joʻnatuvchi ACL qiymati %s noto'g'ri", + "set_acl_failed": "ACL ni oʻrnatib boʻlmadi", + "settings_map_invalid": "Sozlamalar xaritasi ID %s noto'g'ri", + "sieve_error": "Sieve tahlilchisi xatosi: %s", + "spam_learn_error": "Spam oʻrgatish xatosi: %s", + "subject_empty": "Mavzu boʻsh boʻlmasligi kerak", + "target_domain_invalid": "Maqsad domen %s noto'g'ri", + "targetd_not_found": "Maqsad domen %s topilmadi", + "targetd_relay_domain": "Maqsad domen %s relay domen", + "template_exists": "%s shabloni allaqachon mavjud", + "template_id_invalid": "Shablon ID %s noto'g'ri", + "template_name_invalid": "Shablon nomi noto'g'ri", + "temp_error": "Vaqtinchalik xato", + "text_empty": "Matn boʻsh boʻlmasligi kerak", + "tfa_token_invalid": "TFA tokeni noto'g'ri", + "tls_policy_map_dest_invalid": "Siyosat manzili noto'g'ri", + "tls_policy_map_entry_exists": "TLS siyosati xaritasi yozuvi \"%s\" mavjud", + "tls_policy_map_parameter_invalid": "Siyosat parametri noto'g'ri", + "to_invalid": "Qabul qiluvchi boʻsh boʻlmasligi kerak", + "totp_verification_failed": "TOTP tekshiruvi muvaffaqiyatsiz", + "transport_dest_exists": "Transport manzili \"%s\" mavjud", + "webauthn_verification_failed": "WebAuthn tekshiruvi muvaffaqiyatsiz: %s", + "webauthn_authenticator_failed": "Tanlangan autentifikator topilmadi", + "webauthn_publickey_failed": "Tanlangan autentifikator uchun ochiq kalit saqlanmagan", + "webauthn_username_failed": "Tanlangan autentifikator boshqa hisobga tegishli", + "unknown": "Noma'lum xato yuz berdi", + "unknown_tfa_method": "Noma'lum TFA usuli", + "unlimited_quota_acl": "Cheksiz kvota ACL tomonidan taqiqlangan", + "username_invalid": "Foydalanuvchi nomi %s ishlatib boʻlmaydi", + "validity_missing": "Iltimos, amal qilish muddatini tayinlang", + "value_missing": "Iltimos, barcha qiymatlarni kiriting", + "version_invalid": "Versiya %s noto'g'ri", + "yotp_verification_failed": "Yubico OTP tekshiruvi muvaffaqiyatsiz: %s" + }, + "datatables": { + "collapse_all": "Hammasini yigʻish", + "decimal": ".", + "emptyTable": "Jadvalda ma'lumot mavjud emas", + "expand_all": "Hammasini kengaytirish", + "info": "_TOTAL_ tadan _START_ dan _END_ gacha koʻrsatilmoqda", + "infoEmpty": "0 tadan 0 dan 0 gacha koʻrsatilmoqda", + "infoFiltered": "(jami _MAX_ tadan filtrlangan)", + "infoPostFix": "", + "thousands": ",", + "lengthMenu": "_MENU_ ta yozuvni koʻrsatish", + "loadingRecords": "Yuklanmoqda...", + "processing": "Iltimos, kuting...", + "search": "Qidirish:", + "zeroRecords": "Mos keladigan yozuvlar topilmadi", + "paginate": { + "first": "Birinchi", + "last": "Oxirgi", + "next": "Keyingi", + "previous": "Oldingi" + }, + "aria": { + "sortAscending": ": ustunni oʻsish boʻyicha saralash uchun faollashtiring", + "sortDescending": ": ustunni kamayish boʻyicha saralash uchun faollashtiring" + } + }, + "debug": { + "architecture": "Arxitektura", + "chart_this_server": "Diagramma (bu server)", + "containers_info": "Konteyner ma'lumotlari", + "container_running": "Ishlamoqda", + "container_disabled": "Konteyner toʻxtatilgan yoki oʻchirilgan", + "container_stopped": "Toʻxtatilgan", + "cores": "Yadrolar", + "current_time": "Tizim vaqti", + "disk_usage": "Disk foydalanish", + "docs": "Hujjatlar", + "error_show_ip": "Ommaviy IP manzillarini aniqlab boʻlmadi", + "external_logs": "Tashqi jurnallar", + "history_all_servers": "Tarix (barcha serverlar)", + "in_memory_logs": "Xotiradagi jurnallar", + "last_modified": "Oxirgi oʻzgartirilgan", + "log_info": "

    mailcow xotiradagi jurnallar Redis roʻyxatlarida toʻplanadi va har daqiqada hammomilik kamaytirish uchun LOG_LINES (%d) ga qisqartiriladi.\r\n
    Xotiradagi jurnallar doimiy boʻlishi mumkin emas. Xotirada jurnal yurituvchi barcha ilovalar ham Docker daemoniga, demak standart jurnal yuritish drayveriga ham yozadi.\r\n
    Xotiradagi jurnal turi konteynerlar bilan kichik muammolarni nosozliklarni bartaraf etish uchun ishlatilishi kerak.

    \r\n

    Tashqi jurnallar berilgan ilovaning API si orqali toʻplanadi.

    \r\n

    Statik jurnallar asosan faollik jurnallaridir, ular Dockerd ga yozilmaydi, lekin baribir doimiy boʻlishi kerak (API jurnallaridan tashqari).

    ", + "login_time": "Vaqt", + "logs": "Jurnallar", + "memory": "Xotira", + "online_users": "Onlayn foydalanuvchilar", + "restart_container": "Qayta ishga tushirish", + "service": "Xizmat", + "show_ip": "Ommaviy IP ni koʻrsatish", + "size": "Oʻlcham", + "started_at": "Boshlangan vaqt", + "started_on": "Boshlangan sana", + "static_logs": "Statik jurnallar", + "success": "Muvaffaqiyatli", + "system_containers": "Tizim va konteynerlar", + "timezone": "Vaqt mintaqasi", + "uptime": "Ishlash vaqti", + "update_available": "Yangilanish mavjud", + "no_update_available": "Tizim oxirgi versiyada", + "update_failed": "Yangilanishni tekshirib boʻlmadi", + "username": "Foydalanuvchi nomi", + "wip": "Hozir ishlab chiqilmoqda" + }, + "diagnostics": { + "cname_from_a": "Qiymat A/AAAA yozuvidan olingan. Yozuv toʻgʻri resursga ishora qilsa, bu qoʻllab-quvvatlanadi.", + "dns_records": "DNS yozuvlari", + "dns_records_24hours": "DNS ga kiritilgan oʻzgarishlar ushbu sahifada toʻgʻri aks ettirilishi uchun 24 soatgacha vaqt ketishi mumkinligini hisobga oling. Bu DNS yozuvlaringizni qanday sozlash kerakligini osongina koʻrish va barcha yozuvlaringiz DNS da toʻgʻri saqlanganligini tekshirish uchun moʻljallangan.", + "dns_records_data": "Toʻgʻri ma'lumot", + "dns_records_docs": "Iltimos, hujjatlarga ham murojaat qiling.", + "dns_records_name": "Nomi", + "dns_records_status": "Joriy holat", + "dns_records_type": "Tur", + "optional": "Bu yozuv ixtiyoriy." + }, + "edit": { + "acl": "ACL (Ruxsat)", + "active": "Faol", + "admin": "Administratorni tahrirlash", + "advanced_settings": "Kengaytirilgan sozlamalar", + "alias": "Taxallusni tahrirlash", + "allow_from_smtp": "Faqat ushbu IP larga SMTP dan foydalanishga ruxsat berish", + "allow_from_smtp_info": "Barcha joʻnatuvchilarga ruxsat berish uchun boʻsh qoldiring.
    IPv4/IPv6 manzillari va tarmoqlari.", + "allowed_protocols": "Toʻgʻridan-toʻgʻri foydalanuvchi kirishi uchun ruxsat etilgan protokollar (ilova paroli protokollariga taʼsir qilmaydi)", + "app_name": "Ilova nomi", + "app_passwd": "Ilova paroli", + "app_passwd_protocols": "Ilova paroli uchun ruxsat etilgan protokollar", + "automap": "Papkalarni avtomatik xaritalashga harakat qilish (\"Sent items\", \"Sent\" => \"Sent\" va h.k.)", + "backup_mx_options": "Relay sozlamalari", + "bcc_dest_format": "BCC manzili bitta haqiqiy e-pochta manzili boʻlishi kerak.
    Agar bir nechta manzilga nusxa joʻnatish kerak boʻlsa, taxallus yarating va shu yerda ishlating.", + "client_id": "Mijoz ID", + "client_secret": "Mijoz maxfiy soʻzi", + "comment_info": "Maxfiy izoh foydalanuvchiga koʻrinmaydi, ommaviy izoh esa foydalanuvchi koʻrinishida ustiga sichqoncha kelganda maslahat sifatida koʻrsatiladi", + "created_on": "Yaratilgan sana", + "custom_attributes": "Maxsus atributlar", + "delete1": "Tugatilgach manbadan oʻchirish", + "delete2": "Manbada boʻlmagan xabarlarni manzildan oʻchirish", + "delete2duplicates": "Manzildagi dublikatlarni oʻchirish", + "delete_ays": "Iltimos, oʻchirish jarayonini tasdiqlang.", + "description": "Tavsif", + "disable_login": "Kirishni taqiqlash (kiruvchi pochta hali ham qabul qilinadi)", + "domain": "Domenni tahrirlash", + "domain_admin": "Domen administratorini tahrirlash", + "domain_footer": "Domen boʻyicha pastki qism", + "domain_footer_html": "HTML pastki qism", + "domain_footer_info": "Domen boʻyicha pastki qismlar ushbu domendagi manzilga bog'liq barcha chiquvchi xatlarga qoʻshiladi.
    Pastki qism uchun quyidagi oʻzgaruvchilarni ishlatish mumkin:", + "domain_footer_info_vars": { + "auth_user": "{= auth_user =} - MTA tomonidan belgilangan autentifikatsiya qilingan foydalanuvchi nomi", + "from_user": "{= from_user =} - Konvertning Kimdan qismi, masalan \"moo@mailcow.tld\" uchun \"moo\" qaytaradi", + "from_name": "{= from_name =} - Konvertning Kimdan ismi, masalan \"Mailcow <moo@mailcow.tld>\" uchun \"Mailcow\" qaytaradi", + "from_addr": "{= from_addr =} - Konvertning Kimdan manzili qismi", + "from_domain": "{= from_domain =} - Konvertning Kimdan domeni qismi", + "custom": "{= foo =} - Agar pochta qutisida \"bar\" qiymatli \"foo\" maxsus atributi boʻlsa \"bar\" qaytaradi" + }, + "domain_footer_plain": "PLAIN pastki qism", + "domain_footer_skip_replies": "Javob xatlarida pastki qismni e'tiborsiz qoldirish", + "domain_quota": "Domen kvotasi", + "domains": "Domenlar", + "dont_check_sender_acl": "%s domeni uchun joʻnatuvchi tekshiruvini oʻchirish (+ taxallus domenlari)", + "edit_alias_domain": "Taxallus domenini tahrirlash", + "encryption": "Shifrlash", + "exclude": "Obyektlarni istisno qilish (regex)", + "extended_sender_acl": "Tashqi joʻnatuvchi manzillari", + "extended_sender_acl_info": "DKIM domen kaliti mavjud boʻlsa, import qilinishi kerak.
    \r\n Ushbu serverni mos SPF TXT yozuviga qoʻshishni unutmang.
    \r\n Ushbu serverga tashqi manzil bilan ust-ust tushadigan domen yoki taxallus domeni qoʻshilganda, tashqi manzil olib tashlanadi.
    \r\n *@domain.tld sifatida joʻnatishga ruxsat berish uchun @domain.tld dan foydalaning.", + "force_pw_update": "Keyingi kirishda parolni majburiy yangilash", + "force_pw_update_info": "Bu foydalanuvchi faqat %s ga kira oladi. Ilova parollari ishlash holatida qoladi.", + "footer_exclude": "Pastki qismdan istisno qilish", + "full_name": "Toʻliq ism", + "gal": "Global manzillar roʻyxati", + "gal_info": "GAL domenning barcha obyektlarini oʻz ichiga oladi va hech bir foydalanuvchi tomonidan tahrirlanmaydi. Oʻchirilsa, SOGodagi bandlik maʼlumotlari yoʻqoladi! Oʻzgarishlarni qoʻllash uchun SOGoni qayta ishga tushiring.", + "generate": "yaratish", + "grant_types": "Grant turlari", + "hostname": "Xost nomi", + "inactive": "Nofaol", + "internal": "Ichki", + "internal_info": "Ichki taxalluslarga faqat oʻz domeni yoki taxallus domenlaridan kirish mumkin.", + "sender_allowed": "Ushbu taxallus orqali joʻnatishga ruxsat berish", + "sender_allowed_info": "Oʻchirilgan boʻlsa, bu taxallus faqat xat qabul qilishi mumkin. Maxsus pochta qutilariga joʻnatishga ruxsat berish uchun joʻnatuvchi ACL dan foydalaning.", + "kind": "Turi", + "last_modified": "Oxirgi oʻzgartirilgan", + "lookup_mx": "Manzil — MX nomiga mos keladigan muntazam ifoda (.*\\.google\\.com — google.com bilan tugaydigan MX ga yoʻnaltirilgan barcha pochtani ushbu nuqta orqali yoʻnaltirish uchun)", + "mailbox": "Pochta qutisini tahrirlash", + "mailbox_quota_def": "Standart pochta qutisi kvotasi", + "mailbox_relayhost_info": "Faqat pochta qutisi va to'g'ridan-to'g'ri taxalluslarga qoʻllaniladi, domen relay xostini bekor qiladi.", + "mailbox_rename": "Pochta qutisini qayta nomlash", + "mailbox_rename_agree": "Men zaxira nusxasini yaratdim.", + "mailbox_rename_warning": "MUHIM! Pochta qutisini qayta nomlashdan oldin zaxira nusxasi yarating.", + "mailbox_rename_alias": "Taxallusni avtomatik yaratish", + "mailbox_rename_title": "Yangi mahalliy pochta qutisi nomi", + "max_aliases": "Maks. taxalluslar", + "max_mailboxes": "Mumkin boʻlgan maks. pochta qutilari", + "max_quota": "Bitta pochta qutisining maks. kvotasi (MiB)", + "maxage": "Masofadan soʻraladigan xabarlarning maksimal yoshi (kunlarda)
    (0 = yoshni e'tiborsiz qoldirish)", + "maxbytespersecond": "Maks. soniyaga bayt
    (0 = cheksiz)", + "mbox_rl_info": "Bu tezlik chegarasi SASL kirish nomiga qoʻllaniladi, u kirgan foydalanuvchi tomonidan ishlatiladigan har qanday \"from\" manziliga mos keladi. Pochta qutisi tezlik chegarasi domen boʻyicha tezlik chegarasini bekor qiladi.", + "mins_interval": "Oraliq (daqiqa)", + "mta_sts": "MTA-STS", + "mta_sts_info": "MTA-STS — pochta serverlari oʻrtasida xat yetkazib berishni haqiqiy sertifikatli TLS dan foydalanishga majburlovchi standart.
    U DNSSEC mavjud emasligi yoki qoʻllab-quvvatlanmasligi sababli DANE imkoni boʻlmaganda ishlatiladi.
    Eslatma: Agar qabul qiluvchi domen DNSSEC bilan DANE ni qoʻllab-quvvatlasa, DANE har doim afzal koʻriladi — MTA-STS faqat zaxira sifatida ishlaydi.", + "mta_sts_version": "Versiya", + "mta_sts_version_info": "MTA-STS standartining versiyasini belgilaydi — hozirda faqat STSv1 haqiqiy.", + "mta_sts_mode": "Rejim", + "mta_sts_mode_info": "Tanlash uchun uchta rejim mavjud:
    • testing — siyosat faqat kuzatiladi, buzilishlar taʼsir koʻrsatmaydi.
    • enforce — siyosat qatʼiy qoʻllaniladi, haqiqiy TLS siz ulanishlar rad etiladi.
    • none — siyosat eʼlon qilinadi, ammo qoʻllanilmaydi.
    ", + "mta_sts_max_age": "Maks. yosh", + "mta_sts_max_age_info": "Qabul qiluvchi pochta serverlari ushbu siyosatni qayta olishdan oldin keshlashi mumkin boʻlgan vaqt (soniyalarda).", + "mta_sts_mx": "MX server", + "mta_sts_mx_info": "Faqat aniq sanab oʻtilgan pochta server xost nomlariga joʻnatishga ruxsat beradi; joʻnatuvchi MTA DNS MX xost nomi siyosat roʻyxatiga mos kelishini tekshiradi va faqat haqiqiy TLS sertifikati bilan yetkazib berishga ruxsat beradi (MITM ga qarshi himoya qiladi).", + "mta_sts_mx_notice": "Bir nechta MX serverlar belgilanishi mumkin (vergul bilan ajratilgan).", + "multiple_bookings": "Bir nechta bandlovlar", + "none_inherit": "Hech biri / Meros qilib olish", + "nexthop": "Keyingi nuqta", + "password": "Parol", + "password_recovery_email": "Parolni tiklash e-pochtasi", + "password_repeat": "Tasdiqlash paroli (takrorlang)", + "previous": "Oldingi sahifa", + "private_comment": "Maxfiy izoh", + "public_comment": "Ommaviy izoh", + "pushover": "Pushover", + "pushover_evaluate_x_prio": "Yuqori muhimlikdagi xatni eskalatsiya qilish [X-Priority: 1]", + "pushover_info": "Push bildirishnoma sozlamalari %s ga taxalluslar bilan birga (umumiy, umumiy boʻlmagan, teglangan) yetkazib beriladigan barcha toza (spam boʻlmagan) xatlarga qoʻllaniladi.", + "pushover_only_x_prio": "Faqat yuqori muhimlikdagi xatlarni koʻrib chiqish [X-Priority: 1]", + "pushover_sender_array": "Faqat quyidagi joʻnatuvchi e-pochta manzillarini koʻrib chiqish (vergul bilan ajratilgan)", + "pushover_sender_regex": "Quyidagi joʻnatuvchi regexini koʻrib chiqish", + "pushover_text": "Bildirishnoma matni", + "pushover_title": "Bildirishnoma sarlavhasi", + "pushover_sound": "Tovush", + "pushover_vars": "Joʻnatuvchi filtri belgilanmagan boʻlsa, barcha xatlar koʻrib chiqiladi.
    Regex filtrlari va aniq joʻnatuvchi tekshiruvlari alohida belgilanishi mumkin va ketma-ket koʻrib chiqiladi. Ular bir-biriga bogʻliq emas.
    Matn va sarlavha uchun ishlatiladigan oʻzgaruvchilar (ma'lumotlarni himoya qilish siyosatiga e'tibor bering)", + "pushover_verify": "Maʼlumotlarni tekshirish", + "quota_mb": "Kvota (MiB)", + "quota_warning_bcc": "Kvota ogohlantirishi BCC", + "quota_warning_bcc_info": "Ogohlantirishlar alohida nusxa sifatida quyidagi qabul qiluvchilarga yuboriladi. Mavzu mos foydalanuvchi nomi bilan qavs ichida qoʻshib qoʻyiladi, masalan: Kvota ogohlantirishi (user@example.com).", + "ratelimit": "Tezlik chegarasi", + "redirect_uri": "Yoʻnaltirish/Qaytarish URL", + "relay_all": "Barcha qabul qiluvchilarni relay qilish", + "relay_all_info": "↪ Agar barcha qabul qiluvchilarni relay qilmaslikni tanlasangiz, relay qilinishi kerak boʻlgan har bir qabul qiluvchi uchun (\"koʻr\") pochta qutisi qoʻshishingiz kerak boʻladi.", + "relay_domain": "Ushbu domenni relay qilish", + "relay_transport_info": "
    Maʼlumot
    Bu domen uchun maxsus manzilga transport xaritalarini belgilashingiz mumkin. Agar oʻrnatilmagan boʻlsa, MX qidiruvi amalga oshiriladi.", + "relay_unknown_only": "Faqat mavjud boʻlmagan pochta qutilarini relay qilish. Mavjud pochta qutilari mahalliy yetkazib beriladi.", + "relayhost": "Joʻnatuvchiga bogʻliq transportlar", + "remove": "Olib tashlash", + "resource": "Resurs", + "save": "Oʻzgarishlarni saqlash", + "scope": "Qamrov", + "sender_acl": "Quyidagi sifatida joʻnatishga ruxsat berish", + "sender_acl_disabled": "Joʻnatuvchi tekshiruvi oʻchirilgan", + "sender_acl_info": "Agar A pochta qutisi foydalanuvchisi B pochta qutisi foydalanuvchisi sifatida joʻnatishga ruxsat etilgan boʻlsa, joʻnatuvchi manzili SOGo da tanlanadigan \"from\" maydoni sifatida avtomatik koʻrsatilmaydi.
    \r\n B pochta qutisi foydalanuvchisi A pochta qutisi foydalanuvchisi oʻz manzilini joʻnatuvchi sifatida tanlay olishi uchun SOGo da delegatsiya yaratishi kerak. SOGo da pochta qutisini delegatsiya qilish uchun pochta koʻrinishida yuqori chap qismdagi pochta qutisi nomingiz oʻng tomonidagi menyudan (uch nuqta) foydalaning. Bu xatti-harakat taxallus manzillariga qoʻllanmaydi.", + "sieve_desc": "Qisqa tavsif", + "sieve_type": "Filtr turi", + "skipcrossduplicates": "Papkalararo dublikat xabarlarni oʻtkazib yuborish (birinchi kelgan, birinchi xizmatlanadi)", + "sogo_access": "SOGo ga toʻgʻridan-toʻgʻri yoʻnaltirish", + "sogo_access_info": "Tizimga kirgandan keyin foydalanuvchi avtomatik ravishda SOGo ga yoʻnaltiriladi.", + "sogo_visible": "Taxallus SOGo da koʻrinadi", + "sogo_visible_info": "Bu variant faqat SOGo da koʻrsatilishi mumkin boʻlgan obyektlarga taʼsir qiladi (kamida bitta mahalliy pochta qutisiga ishora qiluvchi umumiy yoki umumiy boʻlmagan taxallus manzillari). Yashirilgan boʻlsa, taxallus SOGo da tanlanadigan joʻnatuvchi sifatida koʻrinmaydi.", + "spam_alias": "Vaqt cheklangan taxallus manzillarini yaratish yoki oʻzgartirish", + "spam_filter": "Spam filtri", + "spam_policy": "Elementlarni ruxsat etilgan/qora roʻyxatga qoʻshish yoki olib tashlash", + "spam_score": "Maxsus spam bahosini oʻrnatish", + "subfolder2": "Manzilning subpapkasiga sinxronizatsiya qilish
    (boʻsh = subpapkadan foydalanmang)", + "syncjob": "Sinxronizatsiya vazifasini tahrirlash", + "target_address": "Yoʻnaltirish manzil(lar)i (vergul bilan ajratilgan)", + "target_domain": "Maqsad domen", + "timeout1": "Masofaviy xostga ulanish vaqtining tugashi", + "timeout2": "Mahalliy xostga ulanish vaqtining tugashi", + "title": "Obyektni tahrirlash", + "unchanged_if_empty": "Oʻzgartirilmagan boʻlsa, boʻsh qoldiring", + "username": "Foydalanuvchi nomi", + "validate_save": "Tekshirish va saqlash" + }, + "fido2": { + "confirm": "Tasdiqlash", + "fido2_auth": "FIDO2 bilan kirish", + "fido2_success": "Qurilma muvaffaqiyatli roʻyxatdan oʻtkazildi", + "fido2_validation_failed": "Tekshirish muvaffaqiyatsiz", + "fn": "Doʻstona nom", + "known_ids": "Maʼlum ID lar", + "none": "Oʻchirilgan", + "register_status": "Roʻyxatga olish holati", + "rename": "Qayta nomlash", + "set_fido2": "FIDO2 qurilmasini roʻyxatdan oʻtkazish", + "set_fido2_touchid": "Apple M1 da Touch ID ni roʻyxatdan oʻtkazish", + "set_fn": "Doʻstona nomni oʻrnatish", + "start_fido2_validation": "FIDO2 tekshiruvini boshlash" + }, + "footer": { + "cancel": "Bekor qilish", + "confirm_delete": "Oʻchirishni tasdiqlash", + "delete_now": "Hozir oʻchirish", + "delete_these_items": "Iltimos, quyidagi obyekt ID siga oʻzgarishlaringizni tasdiqlang", + "hibp_check": "haveibeenpwned.com ga qarshi tekshirish", + "hibp_nok": "Mos keldi! Bu xavfli parol boʻlishi mumkin!", + "hibp_ok": "Mos kelmadi.", + "loading": "Iltimos, kuting...", + "nothing_selected": "Hech narsa tanlanmadi", + "restart_container": "Konteynerni qayta ishga tushirish", + "restart_container_info": "Muhim: Yumshoq qayta ishga tushirish biroz vaqt olishi mumkin, iltimos uning tugashini kuting.", + "restart_now": "Hozir qayta ishga tushirish", + "restarting_container": "Konteyner qayta ishga tushirilmoqda, bu biroz vaqt olishi mumkin" + }, + "header": { + "administration": "Konfiguratsiya va tafsilotlar", + "apps": "Ilovalar", + "debug": "Maʼlumot", + "email": "E-pochta", + "mailcow_system": "Tizim", + "mailcow_config": "Konfiguratsiya", + "quarantine": "Karantin", + "restart_netfilter": "Netfilterni qayta ishga tushirish", + "restart_sogo": "SOGoni qayta ishga tushirish", + "user_settings": "Foydalanuvchi sozlamalari" + }, + "info": { + "awaiting_tfa_confirmation": "TFA tasdiqlanishi kutilmoqda", + "no_action": "Amal qoʻllanib boʻlmaydi", + "session_expires": "Sessiyangiz taxminan 15 soniyadan keyin tugaydi" + }, + "login": { + "back_to_mailcow": "mailcow ga qaytish", + "delayed": "Kirish %s soniyaga kechiktirildi.", + "fido2_webauthn": "FIDO2/WebAuthn kirish", + "forgot_password": "> Parolni unutdingizmi?", + "invalid_pass_reset_token": "Parolni tiklash tokeni noto'g'ri yoki muddati tugagan.
    Iltimos, yangi parolni tiklash havolasini soʻrang.", + "login": "Kirish", + "login_linkstext": "Toʻgʻri kirish emasmi?", + "login_usertext": "Foydalanuvchi sifatida kirish", + "login_domainadmintext": "Domen administratori sifatida kirish", + "login_admintext": "Administrator sifatida kirish", + "login_user": "Foydalanuvchi kirishi", + "login_dadmin": "Domen-administratori kirishi", + "login_admin": "Administrator kirishi", + "mobileconfig_info": "Soʻralgan Apple ulanish profilini yuklab olish uchun pochta qutisi foydalanuvchisi sifatida kiring.", + "new_password": "Yangi parol", + "new_password_confirm": "Yangi parolni tasdiqlang", + "other_logins": "yoki quyidagi orqali kiring", + "password": "Parol", + "reset_password": "Parolni tiklash", + "request_reset_password": "Parol oʻzgartirishni soʻrash", + "username": "Foydalanuvchi nomi", + "email": "E-pochta manzili" + }, + "mailbox": { + "action": "Amal", + "activate": "Faollashtirish", + "active": "Faol", + "add": "Qoʻshish", + "add_alias": "Taxallus qoʻshish", + "add_alias_expand": "Taxallusni taxallus domenlari boʻylab kengaytirish", + "add_bcc_entry": "BCC xaritasi qoʻshish", + "add_domain": "Domen qoʻshish", + "add_domain_alias": "Domen taxallusini qoʻshish", + "add_domain_record_first": "Iltimos, avval domen qoʻshing", + "add_filter": "Filtr qoʻshish", + "add_mailbox": "Pochta qutisi qoʻshish", + "add_recipient_map_entry": "Qabul qiluvchi xaritasini qoʻshish", + "add_resource": "Resurs qoʻshish", + "add_template": "Shablon qoʻshish", + "add_tls_policy_map": "TLS siyosati xaritasini qoʻshish", + "address_rewriting": "Manzilni qayta yozish", + "alias": "Taxallus", + "alias_domain_alias_hint": "Taxalluslar domen taxalluslariga avtomatik qoʻllanilmaydi. my-alias@domain taxallus manzili my-alias@alias-domain manzilini qoplamaydi (bu yerda \"alias-domain\" — \"domain\" uchun tasavvur qilingan taxallus domeni).
    Tashqi pochta qutisiga xatni yoʻnaltirish uchun sieve filtridan foydalaning (\"Filtrlar\" yorligʻiga qarang yoki SOGo -> Forwarder dan foydalaning). Yoʻq taxalluslarni avtomatik qoʻshish uchun \"Taxallusni taxallus domenlari boʻylab kengaytirish\" dan foydalaning.", + "alias_domain_backupmx": "Relay domeni uchun taxallus domeni nofaol", + "aliases": "Taxalluslar", + "all_domains": "Barcha domenlar", + "allow_from_smtp": "Faqat ushbu IP larga SMTP dan foydalanishga ruxsat berish", + "allow_from_smtp_info": "Barcha joʻnatuvchilarga ruxsat berish uchun boʻsh qoldiring.
    IPv4/IPv6 manzillari va tarmoqlari.", + "allowed_protocols": "Ruxsat etilgan protokollar", + "backup_mx": "Relay domeni", + "bcc": "BCC", + "bcc_destination": "BCC manzili", + "bcc_destinations": "BCC manzili", + "bcc_info": "BCC xaritalari barcha xabarlarning nusxalarini boshqa manzilga jimgina yoʻnaltirish uchun ishlatiladi. Qabul qiluvchi xaritasi turi yozuvi mahalliy manzil xatning qabul qiluvchisi sifatida ishlaganda ishlatiladi. Joʻnatuvchi xaritalari ham xuddi shu tamoyilga muvofiq ishlaydi.
    \r\n Mahalliy manzilga yetkazib berish muvaffaqiyatsiz boʻlsa, xabar berilmaydi.", + "bcc_local_dest": "Mahalliy manzil", + "bcc_map": "BCC xaritasi", + "bcc_map_type": "BCC turi", + "bcc_maps": "BCC xaritalari", + "bcc_rcpt_map": "Qabul qiluvchi xaritasi", + "bcc_sender_map": "Joʻnatuvchi xaritasi", + "bcc_to_rcpt": "Qabul qiluvchi xaritasi turiga oʻtish", + "bcc_to_sender": "Joʻnatuvchi xaritasi turiga oʻtish", + "bcc_type": "BCC turi", + "booking_null": "Doimo boʻsh sifatida koʻrsatish", + "booking_0_short": "Doimo boʻsh", + "booking_custom": "Maxsus bandlovlar soni bilan qatʼiy cheklash", + "booking_custom_short": "Qatʼiy chegara", + "booking_ltnull": "Cheksiz, lekin bandlangan boʻlsa band sifatida koʻrsatish", + "booking_lt0_short": "Yumshoq chegara", + "catch_all": "Hammasini-qabul qilish", + "created_on": "Yaratilgan sana", + "daily": "Kunlik", + "deactivate": "Oʻchirish", + "description": "Tavsif", + "disable_login": "Kirishni taqiqlash (kiruvchi pochta hali ham qabul qilinadi)", + "disable_x": "Oʻchirish", + "dkim_domains_selector": "Tanlovchi", + "dkim_key_length": "DKIM kalit uzunligi (bit)", + "domain": "Domen", + "domain_admins": "Domen administratorlari", + "domain_aliases": "Domen taxalluslari", + "domain_templates": "Domen shablonlari", + "domain_quota": "Kvota", + "domain_quota_total": "Umumiy domen kvotasi", + "domains": "Domenlar", + "edit": "Tahrirlash", + "empty": "Natijalar yoʻq", + "enable_x": "Yoqish", + "excludes": "Istisnolar", + "filter_table": "Filtr jadvali", + "filters": "Filtrlar", + "fname": "Toʻliq ism", + "force_pw_update": "Keyingi kirishda parolni majburiy yangilash", + "gal": "Global manzillar roʻyxati", + "goto_ham": "ham sifatida oʻrgatish", + "goto_spam": "spam sifatida oʻrgatish", + "hourly": "Soatlik", + "iam": "Identifikatsiya provayderi", + "in_use": "Foydalanilmoqda (%)", + "inactive": "Nofaol", + "insert_preset": "Misol presetini kiritish \"%s\"", + "internal": "Ichki", + "kind": "Turi", + "last_mail_login": "Oxirgi pochta kirishi", + "last_modified": "Oxirgi oʻzgartirilgan", + "last_pw_change": "Oxirgi parol oʻzgarishi", + "last_run": "Oxirgi ishga tushirilgan", + "last_run_reset": "Keyingisini rejalashtirish", + "mailbox": "Pochta qutisi", + "mailbox_defaults": "Standart sozlamalar", + "mailbox_defaults_info": "Yangi pochta qutilari uchun standart sozlamalarni belgilang.", + "mailbox_defquota": "Standart pochta qutisi oʻlchami", + "mailbox_templates": "Pochta qutisi shablonlari", + "mailbox_quota": "Pochta qutisining maks. oʻlchami", + "mailboxes": "Pochta qutilari", + "max_aliases": "Maks. taxalluslar", + "max_mailboxes": "Mumkin boʻlgan maks. pochta qutilari", + "max_quota": "Bitta pochta qutisining maks. kvotasi", + "mins_interval": "Oraliq (daqiqa)", + "msg_num": "Xabar #", + "multiple_bookings": "Bir nechta bandlovlar", + "never": "Hech qachon", + "no": "✕", + "no_record": "%s obyekti uchun yozuv yoʻq", + "no_record_single": "Yozuv yoʻq", + "open_logs": "Jurnallarni ochish", + "owner": "Egasi", + "private_comment": "Maxfiy izoh", + "public_comment": "Ommaviy izoh", + "q_add_header": "Junk papkasiga koʻchirilganda", + "q_all": " Junk papkasiga koʻchirilganda va rad etilganda", + "q_reject": "rad etilganda", + "quarantine_category": "Karantin bildirishnoma turkumi", + "quarantine_notification": "Karantin bildirishnomalari", + "quick_actions": "Amallar", + "recipient": "Qabul qiluvchi", + "recipient_map": "Qabul qiluvchi xaritasi", + "recipient_map_info": "Qabul qiluvchi xaritalari xabar yetkazib berilishidan oldin manzilni almashtirish uchun ishlatiladi.", + "recipient_map_new": "Yangi qabul qiluvchi", + "recipient_map_new_info": "Qabul qiluvchi xaritasi manzili haqiqiy e-pochta manzili yoki domen nomi boʻlishi kerak.", + "recipient_map_old": "Asl qabul qiluvchi", + "recipient_map_old_info": "Qabul qiluvchi xaritasining asl manzili haqiqiy e-pochta manzili yoki domen nomi boʻlishi kerak.", + "recipient_maps": "Qabul qiluvchi xaritalari", + "relay_all": "Barcha qabul qiluvchilarni relay qilish", + "relay_unknown": "Nomaʼlum pochta qutilarini relay qilish", + "remove": "Olib tashlash", + "resources": "Resurslar", + "running": "Ishlamoqda", + "sender": "Joʻnatuvchi", + "set_postfilter": "Postfiltr sifatida belgilash", + "set_prefilter": "Prefiltr sifatida belgilash", + "sieve_info": "Bitta foydalanuvchi uchun bir nechta filtrlarni saqlash mumkin, lekin bir vaqtning oʻzida faqat bitta prefiltr va bitta postfiltr faol boʻlishi mumkin.
    \r\nHar bir filtr tasvirlangan tartibda qayta ishlanadi. Muvaffaqiyatsiz skript ham, chiqarilgan \"keep;\" ham keyingi skriptlarning qayta ishlanishini toʻxtatmaydi. Global sieve skriptlariga oʻzgartirishlar Dovecot ni qayta ishga tushirishni keltirib chiqaradi.

    Global sieve prefilter • Prefilter • Foydalanuvchi skriptlari • Postfilter • Global sieve postfilter", + "sieve_preset_1": "Xavfli boʻlishi mumkin boʻlgan fayl turlari bilan xatlarni rad etish", + "sieve_preset_2": "Aniq joʻnatuvchining xatini har doim oʻqilgan deb belgilash", + "sieve_preset_3": "Jimgina rad etish, keyingi sieve qayta ishlashini toʻxtatish", + "sieve_preset_4": "INBOX ga joylash, sieve filtrlari tomonidan keyingi qayta ishlashni oʻtkazib yuborish", + "sieve_preset_5": "Avto-javob (taʼtil)", + "sieve_preset_6": "Javob bilan xatni rad etish", + "sieve_preset_7": "Yoʻnaltirish va saqlash/tashlash", + "sieve_preset_8": "Aniq joʻnatuvchining xatini yoʻnaltirish, oʻqilgan deb belgilash va subpapkaga saralash", + "sieve_preset_header": "Iltimos, quyidagi misol presetlariga qarang. Batafsil maʼlumot uchun Wikipediaga qarang.", + "sogo_visible": "Taxallus SOGo da koʻrinadi", + "sogo_visible_n": "Taxallusni SOGo da yashirish", + "sogo_visible_y": "Taxallusni SOGo da koʻrsatish", + "spam_aliases": "Vaqt. taxallus", + "stats": "Statistika", + "status": "Holat", + "sync_jobs": "Sinxronizatsiya vazifalari", + "syncjob_check_log": "Jurnalni tekshirish", + "syncjob_last_run_result": "Oxirgi ishga tushirilgan natija", + "syncjob_EX_OK": "Muvaffaqiyatli", + "syncjob_EXIT_CONNECTION_FAILURE": "Ulanish muammosi", + "syncjob_EXIT_TLS_FAILURE": "Shifrlangan ulanish bilan muammo", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Autentifikatsiya muammosi", + "syncjob_EXIT_OVERQUOTA": "Maqsad pochta qutisi kvotadan oshib ketdi", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Masofaviy serverga ulanib boʻlmadi", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Notoʻgʻri foydalanuvchi nomi yoki parol", + "table_size": "Jadval oʻlchami", + "table_size_show_n": "%s ta element koʻrsatish", + "target_address": "Yoʻnaltirish manzili", + "target_domain": "Maqsad domen", + "templates": "Shablonlar", + "template": "Shablon", + "tls_enforce_in": "Kiruvchi TLS ni majburlash", + "tls_enforce_out": "Chiquvchi TLS ni majburlash", + "tls_map_dest": "Manzil", + "tls_map_dest_info": "Misollar: example.org, .example.org, [mail.example.org]:25", + "tls_map_parameters": "Parametrlar", + "tls_map_parameters_info": "Boʻsh yoki parametrlar, masalan: protocols=!SSLv2 ciphers=medium exclude=3DES", + "tls_map_policy": "Siyosat", + "tls_policy_maps": "TLS siyosati xaritalari", + "tls_policy_maps_enforced_tls": "Bu siyosatlar chiquvchi TLS ulanishlarini majburlovchi pochta qutisi foydalanuvchilari uchun ham xatti-harakatni bekor qiladi. Agar quyida siyosat boʻlmasa, bu foydalanuvchilar smtp_tls_mandatory_protocols va smtp_tls_mandatory_ciphers sifatida belgilangan standart qiymatlarni qoʻllaydi.", + "tls_policy_maps_info": "Bu siyosat xaritasi foydalanuvchining TLS siyosati sozlamalaridan mustaqil ravishda chiquvchi TLS transport qoidalarini bekor qiladi.
    \r\n Batafsil maʼlumot uchun \"smtp_tls_policy_maps\" hujjatlarini tekshiring.", + "tls_policy_maps_long": "Chiquvchi TLS siyosati xaritasi bekor qilishlari", + "toggle_all": "Hammasini almashtirish", + "username": "Foydalanuvchi nomi", + "waiting": "Kutilmoqda", + "weekly": "Haftalik", + "yes": "✓" + }, + "oauth2": { + "access_denied": "OAuth2 orqali kirishga ruxsat berish uchun pochta qutisi egasi sifatida kiring.", + "authorize_app": "Ilovaga ruxsat berish", + "deny": "Rad etish", + "permit": "Ilovaga ruxsat berish", + "profile": "Profil", + "profile_desc": "Shaxsiy maʼlumotlarni koʻrish: foydalanuvchi nomi, toʻliq ism, yaratilgan, oʻzgartirilgan, faol", + "scope_ask_permission": "Ilova quyidagi ruxsatlarni soʻradi" + }, + "quarantine": { + "action": "Amal", + "atts": "Ilovalar", + "check_hash": "VT da fayl hashini qidirish", + "confirm": "Tasdiqlash", + "confirm_delete": "Ushbu elementni oʻchirishni tasdiqlang.", + "danger": "Xavf", + "deliver_inbox": "Inbox ga yetkazib berish", + "disabled_by_config": "Joriy tizim konfiguratsiyasi karantin funksiyasini oʻchirib qoʻyadi. Iltimos, karantin elementlari uchun \"pochta qutisi boʻyicha saqlash\" va \"maksimal oʻlcham\"ni oʻrnating.", + "download_eml": "Yuklab olish (.eml)", + "empty": "Natijalar yoʻq", + "high_danger": "Yuqori", + "info": "Maʼlumot", + "junk_folder": "Junk papkasi", + "learn_spam_delete": "Spam sifatida oʻrgatish va oʻchirish", + "low_danger": "Past", + "medium_danger": "Oʻrtacha", + "neutral_danger": "Neytral", + "notified": "Xabar berildi", + "qhandler_success": "Soʻrov tizimga muvaffaqiyatli yuborildi. Endi oynani yopishingiz mumkin.", + "qid": "Rspamd QID", + "qinfo": "Karantin tizimi rad etilgan xatni (joʻnatuvchiga xat yetkazib berilgandek taassurot bermay) hamda pochta qutisining Junk papkasiga nusxa sifatida yetkazib berilgan xatni maʼlumotlar bazasiga saqlaydi.\r\n
    \"Spam sifatida oʻrgatish va oʻchirish\" xabarni Bayesian teoremasi orqali spam sifatida oʻrgatadi va kelajakda oʻxshash xabarlarni rad etish uchun fuzzy hashlarni hisoblaydi.\r\n
    Bir nechta xabarlarni oʻrgatish — tizimingizga qarab — vaqt talab qilishi mumkinligini unutmang.
    Qora roʻyxatdagi elementlar karantindan istisno qilinadi.", + "qitem": "Karantin elementi", + "quarantine": "Karantin", + "quick_actions": "Amallar", + "quick_delete_link": "Tezkor oʻchirish havolasini ochish", + "quick_info_link": "Maʼlumot havolasini ochish", + "quick_release_link": "Tezkor chiqarish havolasini ochish", + "rcpt": "Qabul qiluvchi", + "received": "Qabul qilingan", + "recipients": "Qabul qiluvchilar", + "refresh": "Yangilash", + "rejected": "Rad etilgan", + "release": "Chiqarish", + "release_body": "Xabaringizni ushbu xabarga eml fayl sifatida ilova qildik.", + "release_subject": "Potensial xavfli karantin elementi %s", + "remove": "Olib tashlash", + "rewrite_subject": "Mavzuni qayta yozish", + "rspamd_result": "Rspamd natijasi", + "sender": "Joʻnatuvchi (SMTP)", + "sender_header": "Joʻnatuvchi (\"From\" sarlavhasi)", + "settings_info": "Karantinga olinadigan maksimal elementlar soni: %s
    Maksimal e-pochta oʻlchami: %s MiB", + "show_item": "Elementni koʻrsatish", + "spam": "Spam", + "spam_score": "Baho", + "subj": "Mavzu", + "table_size": "Jadval oʻlchami", + "table_size_show_n": "%s ta element koʻrsatish", + "text_from_html_content": "Mazmun (oʻzgartirilgan html)", + "text_plain_content": "Mazmun (text/plain)", + "toggle_all": "Hammasini almashtirish", + "type": "Tur" + }, + "queue": { + "delete": "Hammasini oʻchirish", + "flush": "Navbatni tozalash", + "info": "Pochta navbati yetkazib berilishini kutayotgan barcha xatlarni oʻz ichiga oladi. Agar e-pochta uzoq vaqt davomida pochta navbatida tursa, u tizim tomonidan avtomatik ravishda oʻchiriladi.
    Tegishli xatning xato xabari xat nima uchun yetkazib berilmaganligi haqida maʼlumot beradi.", + "legend": "Pochta navbati amallari funksiyalari:", + "ays": "Iltimos, joriy navbatdagi barcha elementlarni oʻchirishni tasdiqlang.", + "deliver_mail": "Yetkazib berish", + "deliver_mail_legend": "Tanlangan xatlarni qayta yetkazib berishga harakat qiladi.", + "hold_mail": "Ushlab turish", + "hold_mail_legend": "Tanlangan xatlarni ushlab turadi. (Keyingi yetkazib berish urinishlarining oldini oladi)", + "queue_manager": "Navbat menejeri", + "show_message": "Xabarni koʻrsatish", + "unban": "navbatdan blokdan chiqarish", + "unhold_mail": "Ushlab turishni bekor qilish", + "unhold_mail_legend": "Tanlangan xatlarni yetkazib berish uchun chiqaradi. (Avval ushlab turish talab qilinadi)" + }, + "ratelimit": { + "disabled": "Oʻchirilgan", + "second": "xabarlar / soniya", + "minute": "xabarlar / daqiqa", + "hour": "xabarlar / soat", + "day": "xabarlar / kun" + }, + "start": { + "help": "Yordam panelini koʻrsatish/yashirish", + "imap_smtp_server_auth_info": "Iltimos, toʻliq e-pochta manzilingiz va PLAIN autentifikatsiya mexanizmidan foydalaning.
    \r\nKirish maʼlumotlaringiz server tomonidagi majburiy shifrlash orqali shifrlanadi." + }, + "success": { + "acl_saved": "%s obyekti uchun ACL saqlandi", + "admin_added": "Administrator %s qoʻshildi", + "admin_api_modified": "API ga oʻzgarishlar saqlandi", + "admin_modified": "Administrator oʻzgarishlari saqlandi", + "admin_removed": "Administrator %s olib tashlandi", + "alias_added": "Taxallus manzili %s (%d) qoʻshildi", + "alias_domain_removed": "Taxallus domeni %s olib tashlandi", + "alias_modified": "Taxallus manzili %s ga oʻzgarishlar saqlandi", + "alias_removed": "Taxallus %s olib tashlandi", + "aliasd_added": "Taxallus domeni %s qoʻshildi", + "aliasd_modified": "Taxallus domeni %s ga oʻzgarishlar saqlandi", + "app_links": "Ilova havolalariga oʻzgarishlar saqlandi", + "app_passwd_added": "Yangi ilova paroli qoʻshildi", + "app_passwd_removed": "Ilova paroli ID %s olib tashlandi", + "bcc_deleted": "BCC xaritasi yozuvlari oʻchirildi: %s", + "bcc_edited": "BCC xaritasi yozuvi %s tahrirlandi", + "bcc_saved": "BCC xaritasi yozuvi saqlandi", + "cors_headers_edited": "CORS sozlamalari saqlandi", + "custom_login_modified": "Kirishni sozlash muvaffaqiyatli saqlandi", + "db_init_complete": "Maʼlumotlar bazasini ishga tushirish tugadi", + "delete_filter": "Filtr ID %s oʻchirildi", + "delete_filters": "Filtrlar oʻchirildi: %s", + "deleted_syncjob": "Sinxronizatsiya vazifasi ID %s oʻchirildi", + "deleted_syncjobs": "Sinxronizatsiya vazifalari oʻchirildi: %s", + "dkim_added": "DKIM kaliti %s saqlandi", + "domain_add_dkim_available": "DKIM kaliti allaqachon mavjud edi", + "dkim_duplicated": "%s domeni uchun DKIM kaliti %s ga nusxalandi", + "dkim_removed": "DKIM kaliti %s olib tashlandi", + "domain_added": "Domen %s qoʻshildi", + "domain_admin_added": "Domen administratori %s qoʻshildi", + "domain_admin_modified": "Domen administratori %s ga oʻzgarishlar saqlandi", + "domain_admin_removed": "Domen administratori %s olib tashlandi", + "domain_footer_modified": "Domen pastki qismi %s ga oʻzgarishlar saqlandi", + "domain_modified": "Domen %s ga oʻzgarishlar saqlandi", + "domain_removed": "Domen %s olib tashlandi", + "dovecot_restart_success": "Dovecot muvaffaqiyatli qayta ishga tushirildi", + "eas_reset": "%s foydalanuvchisi uchun ActiveSync qurilmalari tiklandi", + "f2b_banlist_refreshed": "Banlist ID muvaffaqiyatli yangilandi.", + "f2b_modified": "Fail2ban parametrlariga oʻzgarishlar saqlandi", + "forwarding_host_added": "Yoʻnaltirish xosti %s qoʻshildi", + "forwarding_host_removed": "Yoʻnaltirish xosti %s olib tashlandi", + "global_filter_written": "Filtr faylga muvaffaqiyatli yozildi", + "hash_deleted": "Hash oʻchirildi", + "iam_test_connection": "Ulanish muvaffaqiyatli", + "ip_check_opt_in_modified": "IP tekshiruvi muvaffaqiyatli saqlandi", + "item_deleted": "Element %s muvaffaqiyatli oʻchirildi", + "item_released": "Element %s chiqarildi", + "items_deleted": "Element %s muvaffaqiyatli oʻchirildi", + "items_released": "Tanlangan elementlar chiqarildi", + "learned_ham": "ID %s ham sifatida muvaffaqiyatli oʻrgatildi", + "license_modified": "Litsenziyaga oʻzgarishlar saqlandi", + "logged_in_as": "%s sifatida tizimga kirildi", + "mailbox_added": "Pochta qutisi %s qoʻshildi", + "mailbox_modified": "Pochta qutisi %s ga oʻzgarishlar saqlandi", + "mailbox_removed": "Pochta qutisi %s olib tashlandi", + "mailbox_renamed": "Pochta qutisi %s dan %s ga qayta nomlandi", + "nginx_reloaded": "Nginx qayta yuklandi", + "object_modified": "Obyekt %s ga oʻzgarishlar saqlandi", + "password_policy_saved": "Parol siyosati muvaffaqiyatli saqlandi", + "password_changed_success": "Parol muvaffaqiyatli oʻzgartirildi", + "pushover_settings_edited": "Pushover sozlamalari muvaffaqiyatli oʻrnatildi, iltimos maʼlumotlarni tekshiring.", + "qlearn_spam": "Xabar ID %s spam sifatida oʻrgatildi va oʻchirildi", + "queue_command_success": "Navbat buyrugʻi muvaffaqiyatli yakunlandi", + "recipient_map_entry_deleted": "Qabul qiluvchi xaritasi ID %s oʻchirildi", + "recipient_map_entry_saved": "Qabul qiluvchi xaritasi yozuvi \"%s\" saqlandi", + "recovery_email_sent": "Tiklash e-pochtasi %s ga yuborildi", + "relayhost_added": "Xarita yozuvi %s qoʻshildi", + "relayhost_removed": "Xarita yozuvi %s olib tashlandi", + "reset_main_logo": "Standart logotipga qaytarildi", + "resource_added": "Resurs %s qoʻshildi", + "resource_modified": "Pochta qutisi %s ga oʻzgarishlar saqlandi", + "resource_removed": "Resurs %s olib tashlandi", + "rl_saved": "%s obyekti uchun tezlik chegarasi saqlandi", + "rspamd_ui_pw_set": "Rspamd UI paroli muvaffaqiyatli oʻrnatildi", + "saved_settings": "Sozlamalar saqlandi", + "settings_map_added": "Sozlamalar xaritasi yozuvi qoʻshildi", + "settings_map_removed": "Sozlamalar xaritasi ID %s olib tashlandi", + "sogo_profile_reset": "%s foydalanuvchisi uchun SOGo profili tiklandi", + "template_added": "Shablon %s qoʻshildi", + "template_modified": "Shablon %s ga oʻzgarishlar saqlandi", + "template_removed": "Shablon ID %s oʻchirildi", + "tls_policy_map_entry_deleted": "TLS siyosati xaritasi ID %s oʻchirildi", + "tls_policy_map_entry_saved": "TLS siyosati xaritasi yozuvi \"%s\" saqlandi", + "ui_texts": "UI matnlariga oʻzgarishlar saqlandi", + "upload_success": "Fayl muvaffaqiyatli yuklandi", + "verified_fido2_login": "FIDO2 kirishi tasdiqlandi", + "verified_totp_login": "TOTP kirishi tasdiqlandi", + "verified_webauthn_login": "WebAuthn kirishi tasdiqlandi", + "verified_yotp_login": "Yubico OTP kirishi tasdiqlandi" + }, + "tfa": { + "authenticators": "Autentifikatorlar", + "api_register": "%s Yubico Cloud API dan foydalanadi. Iltimos, kalitingiz uchun API kalitini bu yerdan oling", + "confirm": "Tasdiqlash", + "confirm_totp_token": "Iltimos, yaratilgan tokenni kiritib oʻzgarishlaringizni tasdiqlang", + "delete_tfa": "TFA ni oʻchirish", + "disable_tfa": "Keyingi muvaffaqiyatli kirishgacha TFA ni oʻchirish", + "force_tfa": "Kirishda 2FA roʻyxatdan oʻtkazishni majburlash", + "force_tfa_info": "Foydalanuvchidan panelga kirishdan oldin ikki faktorli autentifikatsiyani sozlash talab qilinadi.", + "setup_title": "Ikki faktorli autentifikatsiya talab qilinadi", + "setup_required": "Hisobingiz ikki faktorli autentifikatsiyani talab qiladi. Davom etish uchun 2FA usulini sozlang.", + "cancel_setup": "Bekor qilish va chiqish", + "enter_qr_code": "Agar qurilmangiz QR kodlarni skanerlay olmasa, TOTP kodingiz", + "error_code": "Xato kodi", + "init_webauthn": "Ishga tushirilmoqda, iltimos kuting...", + "key_id": "Qurilmangiz uchun identifikator", + "key_id_totp": "Kalitingiz uchun identifikator", + "none": "Oʻchirish", + "reload_retry": "- (xato davom etsa, brauzerni qayta yuklang)", + "scan_qr_code": "Iltimos, quyidagi kodni autentifikator ilovangiz bilan skanerlang yoki kodni qoʻlda kiriting.", + "select": "Iltimos, tanlang", + "set_tfa": "Ikki faktorli autentifikatsiya usulini oʻrnatish", + "start_webauthn_validation": "Tekshiruvni boshlash", + "tfa": "Ikki faktorli autentifikatsiya", + "tfa_token_invalid": "TFA tokeni noto'g'ri", + "totp": "Vaqt asosidagi OTP (Google Authenticator, Authy va h.k.)", + "u2f_deprecated": "Sizning kalitingiz eskirgan U2F usulidan foydalanib roʻyxatdan oʻtkazilganga oʻxshaydi. Biz siz uchun ikki faktorli autentifikatsiyani oʻchirib qoʻyamiz va kalitingizni oʻchiramiz.", + "u2f_deprecated_important": "Iltimos, kalitingizni admin panelida yangi WebAuthn usuli bilan roʻyxatdan oʻtkazing.", + "webauthn": "WebAuthn autentifikatsiya", + "waiting_usb_auth": "USB qurilmasi kutilmoqda...

    Iltimos, hozir USB qurilmangizdagi tugmani bosing.", + "waiting_usb_register": "USB qurilmasi kutilmoqda...

    Iltimos, yuqorida parolingizni kiriting va USB qurilmangizdagi tugmani bosib roʻyxatga olinishingizni tasdiqlang.", + "yubi_otp": "Yubico OTP autentifikatsiya" + }, + "user": { + "action": "Amal", + "active": "Faol", + "active_sieve": "Faol filtr", + "advanced_settings": "Kengaytirilgan sozlamalar", + "alias": "Taxallus", + "alias_create_random": "Tasodifiy taxallus yaratish", + "alias_extend_all": "Taxalluslarni 1 soatga uzaytirish", + "alias_full_date": "d.m.Y, H:i:s T", + "alias_remove_all": "Barcha taxalluslarni olib tashlash", + "alias_select_validity": "Amal qilish muddati", + "alias_time_left": "Qolgan vaqt", + "alias_valid_until": "Amal qiladi (gacha)", + "aliases_also_send_as": "Foydalanuvchi sifatida ham joʻnatishga ruxsat berilgan", + "aliases_send_as_all": "Quyidagi domen(lar) va ularning taxallus domenlari uchun joʻnatuvchi kirishini tekshirmaslik", + "app_hint": "Ilova parollari sizning IMAP, SMTP, CalDAV, CardDAV va EAS kirishi uchun muqobil parollardir. Foydalanuvchi nomi oʻzgarmaydi. SOGo webmail ilova parollari orqali mavjud emas.", + "allowed_protocols": "Ruxsat etilgan protokollar", + "app_name": "Ilova nomi", + "app_passwds": "Ilova parollari", + "apple_connection_profile": "Apple ulanish profili", + "apple_connection_profile_complete": "Bu ulanish profili Apple qurilmasi uchun IMAP va SMTP parametrlari hamda CalDAV (kalendarlar) va CardDAV (kontaktlar) yoʻllarini oʻz ichiga oladi.", + "apple_connection_profile_mailonly": "Bu ulanish profili Apple qurilmasi uchun IMAP va SMTP konfiguratsiya parametrlarini oʻz ichiga oladi.", + "apple_connection_profile_with_app_password": "Yangi ilova paroli yaratiladi va profilga qoʻshiladi, shunda qurilmangizni sozlashda parol kiritish kerak boʻlmaydi. Iltimos, faylni ulashmang, chunki u pochta qutingizga toʻliq kirish huquqini beradi.", + "attribute": "Atribut", + "authentication": "Autentifikatsiya", + "change_password": "Parolni oʻzgartirish", + "change_password_hint_app_passwords": "Hisobingizda %d ta ilova paroli mavjud va ular oʻzgartirilmaydi. Ularni boshqarish uchun Ilova parollari yorligʻiga oʻting.", + "clear_recent_successful_connections": "Koʻrilgan muvaffaqiyatli ulanishlarni tozalash", + "client_configuration": "E-pochta mijozlari va smartfonlar uchun konfiguratsiya qoʻllanmalarini koʻrsatish", + "create_app_passwd": "Ilova paroli yaratish", + "create_syncjob": "Yangi sinxronizatsiya vazifasi yaratish", + "created_on": "Yaratilgan sana", + "daily": "Kunlik", + "day": "kun", + "description": "Tavsif", + "delete_ays": "Iltimos, oʻchirish jarayonini tasdiqlang.", + "direct_aliases": "Toʻgʻridan-toʻgʻri taxallus manzillari", + "direct_aliases_desc": "Toʻgʻridan-toʻgʻri taxallus manzillariga spam filtri va TLS siyosati sozlamalari taʼsir qiladi.", + "direct_protocol_access": "Bu pochta qutisi foydalanuvchisi quyidagi protokollar va ilovalarga toʻgʻridan-toʻgʻri, tashqi kirishga ega. Bu sozlama administratoringiz tomonidan boshqariladi. Maxsus protokollar va ilovalarga kirishga ruxsat berish uchun ilova parollarini yaratish mumkin.
    \"Webmail\" tugmasi SOGo ga yagona kirish (SSO) imkonini beradi va doimo mavjud.", + "eas_reset": "ActiveSync qurilma keshini tiklash", + "eas_reset_help": "Koʻp hollarda qurilma keshini tiklash buzilgan ActiveSync profilini tiklashga yordam beradi.
    Diqqat: Barcha elementlar qayta yuklab olinadi!", + "eas_reset_now": "Hozir tiklash", + "edit": "Tahrirlash", + "email": "E-pochta", + "email_and_dav": "E-pochta, kalendarlar va kontaktlar", + "empty": "Natijalar yoʻq", + "encryption": "Shifrlash", + "excludes": "Istisnolar", + "expire_in": "Tugaydi", + "expire_never": "Hech qachon tugamasin", + "fido2_webauthn": "FIDO2/WebAuthn", + "forever": "Abadiy", + "force_pw_update": "Guruh dasturi xizmatlariga kirish uchun siz yangi parol oʻrnatishingiz kerak.", + "from": "dan", + "generate": "yaratish", + "hour": "soat", + "hourly": "Soatlik", + "hours": "soat", + "in_use": "Foydalanilmoqda", + "interval": "Oraliq", + "is_catch_all": "Domen(lar) uchun Hammasini-qabul qilish", + "last_mail_login": "Oxirgi pochta kirishi", + "last_pw_change": "Oxirgi parol oʻzgarishi", + "last_run": "Oxirgi ishga tushirilgan", + "last_ui_login": "Oxirgi UI kirishi", + "loading": "Yuklanmoqda...", + "login_history": "Kirish tarixi", + "mailbox": "Pochta qutisi", + "mailbox_details": "Tafsilotlar", + "mailbox_general": "Umumiy", + "mailbox_settings": "Sozlamalar", + "messages": "xabarlar", + "month": "oy", + "months": "oy", + "never": "Hech qachon", + "new_password": "Yangi parol", + "new_password_repeat": "Tasdiqlash paroli (takrorlang)", + "no_active_filter": "Faol filtr mavjud emas", + "no_last_login": "Oxirgi UI kirishi haqida maʼlumot yoʻq", + "no_record": "Yozuv yoʻq", + "open_logs": "Jurnallarni ochish", + "open_webmail_sso": "Webmail", + "overview": "Umumiy koʻrinish", + "password": "Parol", + "password_now": "Joriy parol (oʻzgarishlarni tasdiqlang)", + "pw_update_required": "Hisobingiz parol oʻzgartirishni talab qiladi. Davom etish uchun yangi parolni oʻrnating.", + "password_repeat": "Parol (takrorlang)", + "password_reset_info": "Agar parolni tiklash uchun e-pochta kiritilmagan boʻlsa, bu funksiyadan foydalanib boʻlmaydi.", + "protocols": "Protokollar", + "pushover_evaluate_x_prio": "Yuqori muhimlikdagi xatni eskalatsiya qilish [X-Priority: 1]", + "pushover_info": "Push bildirishnoma sozlamalari %s ga taxalluslar bilan birga (umumiy, umumiy boʻlmagan, teglangan) yetkazib beriladigan barcha toza (spam boʻlmagan) xatlarga qoʻllaniladi.", + "pushover_only_x_prio": "Faqat yuqori muhimlikdagi xatlarni koʻrib chiqish [X-Priority: 1]", + "pushover_sender_array": "Quyidagi joʻnatuvchi e-pochta manzillarini koʻrib chiqish (vergul bilan ajratilgan)", + "pushover_sender_regex": "Joʻnatuvchilarni quyidagi regex orqali mos kelishini tekshirish", + "pushover_text": "Bildirishnoma matni", + "pushover_title": "Bildirishnoma sarlavhasi", + "pushover_sound": "Tovush", + "pushover_vars": "Joʻnatuvchi filtri belgilanmagan boʻlsa, barcha xatlar koʻrib chiqiladi.
    Regex filtrlari va aniq joʻnatuvchi tekshiruvlari alohida belgilanishi mumkin va ketma-ket koʻrib chiqiladi. Ular bir-biriga bogʻliq emas.
    Matn va sarlavha uchun ishlatiladigan oʻzgaruvchilar (ma'lumotlarni himoya qilish siyosatiga e'tibor bering)", + "pushover_verify": "Maʼlumotlarni tekshirish", + "pw_recovery_email": "Parolni tiklash e-pochtasi", + "q_add_header": "Junk papkasi", + "q_all": "Barcha turkumlar", + "q_reject": "Rad etilgan", + "quarantine_category": "Karantin bildirishnoma turkumi", + "quarantine_category_info": "\"Rad etilgan\" bildirishnoma turkumi rad etilgan xatlarni oʻz ichiga oladi, \"Junk papkasi\" esa Junk papkasiga joylashtirilgan xatlar haqida foydalanuvchini xabardor qiladi.", + "quarantine_notification": "Karantin bildirishnomalari", + "quarantine_notification_info": "Bildirishnoma yuborilgandan keyin elementlar \"xabar berildi\" deb belgilanadi va bu element uchun keyingi bildirishnomalar yuborilmaydi.", + "recent_successful_connections": "Koʻrilgan muvaffaqiyatli ulanishlar", + "remove": "Olib tashlash", + "running": "Ishlamoqda", + "save": "Oʻzgarishlarni saqlash", + "save_changes": "Oʻzgarishlarni saqlash", + "sender_acl_disabled": "Joʻnatuvchi tekshiruvi oʻchirilgan", + "shared_aliases": "Umumiy taxallus manzillari", + "shared_aliases_desc": "Umumiy taxalluslarga foydalanuvchining maxsus sozlamalari (spam filtri yoki shifrlash siyosati kabi) taʼsir qilmaydi. Mos spam filtrlarini faqat administrator domen boʻyicha siyosat sifatida sozlashi mumkin.", + "show_sieve_filters": "Faol foydalanuvchi sieve filtrini koʻrsatish", + "sogo_profile_reset": "SOGo profilini tiklash", + "sogo_profile_reset_help": "Bu foydalanuvchining SOGo profilini yoʻq qiladi va barcha kontakt va kalendar maʼlumotlarini qaytarib boʻlmas tarzda oʻchiradi.", + "sogo_profile_reset_now": "Profilni hozir tiklash", + "spam_aliases": "Spam e-pochta taxalluslari", + "spam_aliases_info": "Spam taxallusi — bu haqiqiy e-pochta manzillarini himoya qilish uchun ishlatilishi mumkin boʻlgan vaqtinchalik e-pochta manzilidir.
    Ixtiyoriy ravishda muddat tugash vaqtini belgilash mumkin, shunda taxallus belgilangan davrdan keyin avtomatik oʻchirib qoʻyiladi, suiisteʼmol qilingan yoki tarqalib ketgan manzillarni samarali tarzda chiqarib tashlaydi.", + "spam_score_reset": "Server standartiga qaytarish", + "spamfilter": "Spam filtri", + "spamfilter_behavior": "Baholash", + "spamfilter_bl": "Qora roʻyxat", + "spamfilter_bl_desc": "Qora roʻyxatdagi e-pochta manzillari har doim spam sifatida tasniflanadi va rad etiladi. Rad etilgan xat karantinga nusxalanmaydi. Joker belgilar ishlatilishi mumkin. Filtr faqat toʻgʻridan-toʻgʻri taxalluslarga (bitta maqsad pochta qutisiga ega taxalluslarga) qoʻllaniladi, hammasini-qabul qilish taxalluslari va pochta qutisining oʻzi istisno qilinadi.", + "spamfilter_default_score": "Standart qiymatlar", + "spamfilter_green": "Yashil: bu xabar spam emas", + "spamfilter_hint": "Birinchi qiymat \"past spam bahosi\"ni tavsiflaydi, ikkinchisi \"yuqori spam bahosi\"ni ifodalaydi.", + "spamfilter_red": "Qizil: bu xabar spam va server tomonidan rad etiladi", + "spamfilter_table_action": "Amal", + "spamfilter_table_add": "Element qoʻshish", + "spamfilter_table_domain_policy": "n/a (domen siyosati)", + "spamfilter_table_empty": "Koʻrsatish uchun ma'lumot yoʻq", + "spamfilter_table_remove": "olib tashlash", + "spamfilter_table_rule": "Qoida", + "spamfilter_wl": "Oq roʻyxat", + "spamfilter_wl_desc": "Oq roʻyxatdagi e-pochta manzillari hech qachon spam sifatida tasniflanmaydi. Joker belgilar ishlatilishi mumkin. Filtr faqat toʻgʻridan-toʻgʻri taxalluslarga (bitta maqsad pochta qutisiga ega taxalluslarga) qoʻllaniladi, hammasini-qabul qilish taxalluslari va pochta qutisining oʻzi istisno qilinadi.", + "spamfilter_yellow": "Sariq: bu xabar spam boʻlishi mumkin, spam sifatida belgilanadi va Junk papkangizga koʻchiriladi", + "status": "Holat", + "sync_jobs": "Sinxronizatsiya vazifalari", + "syncjob_check_log": "Jurnalni tekshirish", + "syncjob_last_run_result": "Oxirgi ishga tushirilgan natija", + "syncjob_EX_OK": "Muvaffaqiyatli", + "syncjob_EXIT_CONNECTION_FAILURE": "Ulanish muammosi", + "syncjob_EXIT_TLS_FAILURE": "Shifrlangan ulanish bilan muammo", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Autentifikatsiya muammosi", + "syncjob_EXIT_OVERQUOTA": "Maqsad pochta qutisi kvotadan oshib ketdi", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Masofaviy serverga ulanib boʻlmadi", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Notoʻgʻri foydalanuvchi nomi yoki parol", + "tag_handling": "Teglangan xat uchun ishlovni oʻrnatish", + "tag_help_example": "Teglangan e-pochta manzili misoli: me+Facebook@example.org", + "tag_help_explain": "Subpapkada: tegga qarab nomlangan yangi subpapka INBOX ostida yaratiladi (\"INBOX/Facebook\").
    \r\nMavzuda: teg nomi xatning mavzusiga oldindan qoʻshiladi, misol: \"[Facebook] Mening yangiliklarim\".", + "tag_in_none": "Hech narsa qilmaslik", + "tag_in_subfolder": "Subpapkada", + "tag_in_subject": "Mavzuda", + "text": "Matn", + "tfa_info": "Ikki faktorli autentifikatsiya hisobingizni himoya qilishga yordam beradi. Uni yoqsangiz, ikki faktorli autentifikatsiyani qoʻllab-quvvatlamaydigan ilovalar yoki xizmatlarga (masalan, pochta mijozlari) kirish uchun ilova parollari kerak boʻladi.", + "title": "Sarlavha", + "tls_enforce_in": "Kiruvchi TLS ni majburlash", + "tls_enforce_out": "Chiquvchi TLS ni majburlash", + "tls_policy": "Shifrlash siyosati", + "tls_policy_warning": "Diqqat: Agar shifrlangan pochta transferini majburlashga qaror qilsangiz, e-pochtalarni yoʻqotishingiz mumkin.
    Siyosatga mos kelmaydigan xabarlar pochta tizimi tomonidan qatʼiy muvaffaqiyatsizlik bilan qaytariladi.
    Bu variant sizning asosiy e-pochta manzilingiz (kirish nomingiz), taxallus domenlaridan olingan barcha manzillar va faqat ushbu pochta qutisi maqsad sifatida boʻlgan taxallus manzillariga qoʻllaniladi.", + "user_settings": "Foydalanuvchi sozlamalari", + "username": "Foydalanuvchi nomi", + "value": "Qiymat", + "verify": "Tasdiqlash", + "waiting": "Kutilmoqda", + "week": "hafta", + "weekly": "Haftalik", + "weeks": "hafta", + "with_app_password": "ilova paroli bilan", + "year": "yil", + "years": "yil" + }, + "warning": { + "cannot_delete_self": "Tizimga kirgan foydalanuvchini oʻchirib boʻlmaydi", + "domain_added_sogo_failed": "Domen qoʻshildi, ammo SOGoni qayta ishga tushirib boʻlmadi, iltimos server jurnallaringizni tekshiring.", + "dovecot_restart_failed": "Dovecot qayta ishga tushirib boʻlmadi, iltimos jurnallarni tekshiring", + "fuzzy_learn_error": "Fuzzy hash oʻrgatish xatosi: %s", + "hash_not_found": "Hash topilmadi yoki allaqachon oʻchirilgan", + "ip_invalid": "Notoʻgʻri IP oʻtkazib yuborildi: %s", + "is_not_primary_alias": "Asosiy boʻlmagan taxallus oʻtkazib yuborildi: %s", + "no_active_admin": "Oxirgi faol administratorni oʻchirib boʻlmaydi", + "quota_exceeded_scope": "Domen kvotasi oshib ketdi: Bu domen doirasida faqat cheksiz pochta qutilari yaratilishi mumkin.", + "session_token": "Forma tokeni noto'g'ri: Token mos kelmadi", + "session_ua": "Forma tokeni noto'g'ri: User-Agent tekshiruvi xatosi" + } +} diff --git a/data/web/lang/lang.vi-vn.json b/data/web/lang/lang.vi-vn.json index 0e9e6a2c4..42813fd25 100644 --- a/data/web/lang/lang.vi-vn.json +++ b/data/web/lang/lang.vi-vn.json @@ -111,7 +111,8 @@ "timeout2": "Thời gian chờ kết nối đến máy chủ cục bộ", "username": "Tên người dùng", "validate": "Xác thực", - "validation_success": "Xác thực thành công" + "validation_success": "Xác thực thành công", + "sender_allowed": "Cho phép gửi dưới bí danh này" }, "admin": { "access": "Truy cập", @@ -267,10 +268,10 @@ "includes": "Bao gồm những người nhận này", "ip_check": "Kiểm tra IP", "ip_check_disabled": "Kiểm tra IP đã bị vô hiệu hóa. Bạn có thể bật nó trong
    Hệ thống > Cấu hình > Tùy chọn > Tùy chỉnh", - "ip_check_opt_in": "Chọn tham gia sử dụng dịch vụ bên thứ ba ipv4.mailcow.email và ipv6.mailcow.email để phân giải địa chỉ IP bên ngoài.", + "ip_check_opt_in": "Đăng ký tham gia sử dụng dịch vụ bên thứ ba dùng ipv4.mailcow.email và ipv6.mailcow.email để phân giải địa chỉ IP bên ngoài.", "is_mx_based": "Dựa trên MX", "last_applied": "Áp dụng lần cuối", - "license_info": "Giấy phép không bắt buộc nhưng giúp phát triển thêm.
    Đăng ký GUID của bạn tại đây hoặc mua hỗ trợ cho cài đặt mailcow của bạn.", + "license_info": "Giấy phép tuy không bắt buộc nhưng giúp phát triển thêm.
    Đăng ký GUID của bạn tại đây hoặc mua hỗ trợ cho cài đặt mailcow của bạn.", "link": "Liên kết", "loading": "Vui lòng đợi...", "login_time": "Thời gian đăng nhập", @@ -555,7 +556,9 @@ "validity_missing": "Vui lòng gán thời hạn hiệu lực", "value_missing": "Vui lòng cung cấp tất cả các giá trị", "version_invalid": "Phiên bản %s không hợp lệ", - "yotp_verification_failed": "Xác thực Yubico OTP thất bại: %s" + "yotp_verification_failed": "Xác thực Yubico OTP thất bại: %s", + "tfa_removal_blocked": "Xác thực hai yếu tố không thể bị xóa vì đây là yêu cầu bắt buộc đối với tài khoản của bạn.", + "quarantine_category_invalid": "Danh mục cách ly phải là một trong các loại sau : add_header, reject, all." }, "datatables": { "collapse_all": "Thu gọn tất cả", @@ -578,7 +581,9 @@ "aria": { "sortAscending": ": kích hoạt để sắp xếp cột tăng dần", "sortDescending": ": kích hoạt để sắp xếp cột giảm dần" - } + }, + "decimal": ".", + "thousands": "," }, "debug": { "architecture": "Kiến trúc", @@ -692,6 +697,19 @@ "internal_info": "Bí danh nội bộ chỉ có thể truy cập từ tên miền sở hữu hoặc tên miền bí danh.", "kind": "Loại", "last_modified": "Sửa đổi lần cuối", - "lookup_mx": "Đích là một biểu thức chính quy để khớp với tên MX (.*.google.com để định tuyến tất cả thư nhắm đến MX kết thúc bằng google.com qua bước nhảy này)" + "lookup_mx": "Đích là một biểu thức chính quy để khớp với tên MX (.*.google.com để định tuyến tất cả thư nhắm đến MX kết thúc bằng google.com qua bước nhảy này)", + "sender_allowed": "Cho phép gửi đi bằng bí danh", + "sender_allowed_info": "Nếu bị vô hiệu hóa, bí danh này chỉ có thể nhận thư. Sử dụng ACL của người gửi để ghi đè và cấp quyền gửi cho các hộp thư cụ thể.", + "mailbox": "Chỉnh sửa hộp thư", + "mailbox_quota_def": "Hạn mức hộp thư mặc định", + "mailbox_relayhost_info": "Chỉ áp dụng cho hộp thư và các bí danh trực tiếp, thao tác này sẽ ghi đè lên máy chủ chuyển tiếp tên miền.", + "mailbox_rename": "Đổi tên hộp thư", + "mailbox_rename_agree": "Tôi đã tạo bản sao lưu.", + "mailbox_rename_warning": "QUAN TRỌNG! Hãy tạo bản sao lưu trước khi đổi tên hộp thư.", + "mailbox_rename_alias": "Tạo tự động bí danh", + "mailbox_rename_title": "Tên mới của hộp thư cục bộ", + "max_aliases": "Số lượng Bí danh tối đa", + "max_mailboxes": "Số lượng hộp thư tối đa có thể có", + "max_quota": "Dung lượng tối đa cho mỗi hộp thư (MiB)" } } diff --git a/data/web/lang/lang.zh-cn.json b/data/web/lang/lang.zh-cn.json index fe225a5df..2a1a080b9 100644 --- a/data/web/lang/lang.zh-cn.json +++ b/data/web/lang/lang.zh-cn.json @@ -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": "等待 USB 设备中...

    现在请触碰你的 WebAuthn USB 设备上的按钮。", "waiting_usb_register": "等待 USB 设备中...

    请在上方输入你的密码并请触碰你的 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": "垃圾邮件别名是一种临时电子邮件地址,可用于保护真实电子邮件地址。
    还可以选择设置过期时间,以便在设定的时间后自动停用别名,从而有效地销毁被滥用或泄露的地址。" + "spam_aliases_info": "垃圾邮件别名是一种临时电子邮件地址,可用于保护真实电子邮件地址。
    还可以选择设置过期时间,以便在设定的时间后自动停用别名,从而有效地销毁被滥用或泄露的地址。", + "pw_update_required": "你的账户需要修改一次密码。请设置一个新密码以继续。" }, "warning": { "cannot_delete_self": "不能删除已登录的用户", diff --git a/data/web/lang/lang.zh-tw.json b/data/web/lang/lang.zh-tw.json index 592efa809..e7310be7a 100644 --- a/data/web/lang/lang.zh-tw.json +++ b/data/web/lang/lang.zh-tw.json @@ -24,7 +24,7 @@ "sogo_access": "管理 SOGo 存取權", "sogo_profile_reset": "重設 SOGo 個人資料", "spam_alias": "臨時別名", - "spam_policy": "黑名單/白名單", + "spam_policy": "封鎖名單/允許名單", "spam_score": "垃圾郵件分數", "syncjobs": "同步任務", "tls_policy": "TLS 規則", @@ -1180,7 +1180,7 @@ "no_last_login": "沒有最後 UI 登入訊息", "no_record": "沒有紀錄", "open_logs": "開啟日誌", - "open_webmail_sso": "网络邮件", + "open_webmail_sso": "網頁信箱", "password": "密碼", "password_now": "目前密碼 (確認更改)", "password_repeat": "密碼 (再次輸入)", diff --git a/data/web/mobileconfig.php b/data/web/mobileconfig.php index 7c0ead7f5..c57c48857 100644 --- a/data/web/mobileconfig.php +++ b/data/web/mobileconfig.php @@ -65,6 +65,7 @@ if (isset($_GET['app_password'])) { $attr['protocols'][] = 'dav_access'; } app_passwd("add", $attr); + $password = htmlspecialchars($password, ENT_NOQUOTES); } else { $app_password = false; } diff --git a/data/web/oauth/authorize.php b/data/web/oauth/authorize.php index 1fdcd0335..2bde21d36 100644 --- a/data/web/oauth/authorize.php +++ b/data/web/oauth/authorize.php @@ -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']); diff --git a/data/web/sogo-auth.php b/data/web/sogo-auth.php index 3e3eb5f79..bc9694d8e 100644 --- a/data/web/sogo-auth.php +++ b/data/web/sogo-auth.php @@ -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'; @@ -25,16 +29,8 @@ if (isset($_SERVER['PHP_AUTH_USER'])) { elseif (preg_match('/^(\/SOGo|)\/Microsoft-Server-ActiveSync.*/', $original_uri) === 1) { $service = 'EAS'; } - if (empty($password)) { - $remote = get_remote_ip(); - $docker_ipv4_network = getenv('IPV4_NETWORK'); - if ($remote == "{$docker_ipv4_network}.246") { - $login_check = 'user'; - $password = file_get_contents("/etc/sogo-sso/sogo-sso.pass"); - } - } else { - $login_check = check_login($username, $password, array('service' => $service)); - } + + $login_check = check_login($username, $password, array('service' => $service)); if ($login_check === 'user') { header("X-User: $username"); header("X-Auth: Basic ".base64_encode("$username:$password")); @@ -58,6 +54,16 @@ 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: /"); + exit; + } // register username in session $_SESSION[$session_var_user_allowed][] = $login; // set dual login @@ -83,13 +89,12 @@ 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')) { include_once $_SERVER['DOCUMENT_ROOT'] . '/inc/vars.local.inc.php'; } - require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php'; $email_list = array( @@ -103,7 +108,8 @@ elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HT filter_var($email, FILTER_VALIDATE_EMAIL) && is_array($_SESSION[$session_var_user_allowed]) && in_array($email, $_SESSION[$session_var_user_allowed]) && - !$_SESSION['pending_pw_update'] + !$_SESSION['pending_pw_update'] && + !$_SESSION['pending_tfa_setup'] ) { $username = $email; $password = file_get_contents("/etc/sogo-sso/sogo-sso.pass"); diff --git a/data/web/templates/admin/tab-config-customize.twig b/data/web/templates/admin/tab-config-customize.twig index aecf1846e..f28e4c130 100644 --- a/data/web/templates/admin/tab-config-customize.twig +++ b/data/web/templates/admin/tab-config-customize.twig @@ -7,7 +7,33 @@ {{ lang.admin.customize }}
    - {{ lang.admin.change_logo }}
    + {{ lang.admin.ui_theme }}
    +
    {{ lang.admin.ui_theme_info|raw }}
    +
    +
    +
    +

    +
    +
    + {% if ui_themes %} +
      + {% for key, theme in ui_themes %} +
    • {{ theme.name }} {{ key }}{% if theme.dark %} {% endif %}{% if theme.description %} - {{ theme.description }}{% endif %}
    • + {% endfor %} +
    + {% else %} +

    {{ lang.admin.ui_theme_none }}

    + {% endif %} +

    + +

    +
    + {{ lang.admin.change_logo }}

    {{ lang.admin.logo_info }}

    diff --git a/data/web/templates/admin_index.twig b/data/web/templates/admin_index.twig index 0dcb9145e..0507786cb 100644 --- a/data/web/templates/admin_index.twig +++ b/data/web/templates/admin_index.twig @@ -6,9 +6,9 @@
    -
    +