diff --git a/.gitignore b/.gitignore index 1a5d5ae..a62cf45 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,14 @@ # General bkp/* .cursor +**/.vscode/ .env **/.env +*.log +*.out +*.tmp +HEALTH_CHECK_REPORT.md +/core/mta-sts/.well-known/mta-sts.txt secrets/* !secrets/.gitkeep **/.git @@ -96,4 +102,5 @@ mailcow/create_cold_standby.sh nextcloud/hooks/post-installation/set-configs.sh **/volumes/ mailcow/data/conf/dovecot/auth/passwd-verify.lua -mailcow/data/conf/postfix/main.cf \ No newline at end of file +mailcow/data/conf/postfix/main.cf +passwd-verify.lua diff --git a/README.md b/README.md index eeca8d2..69cc16d 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,29 @@ This repository contains a productive stack of open-source applications for team collaboration and communication. The stack includes: ### Core Infrastructure -- **Traefik**: Edge router that handles routing and load balancing for all services +- **Traefik**: Edge router that handles routing, TLS (Let's Encrypt) and load balancing for all services - **PostgreSQL**: Relational database for applications requiring PostgreSQL - **MariaDB**: MySQL-compatible database for applications requiring MySQL - **Adminer**: Database management tool for easy database administration +- **WireGuard**: VPN gateway; SSH is reachable only from within the VPN subnet +- **MTA-STS**: Static policy host for mail transport security + +### Code Hosting & CI +- **Forgejo**: Self-hosted Git forge (replaces GitLab) at `git.${DOMAIN}`, SSH on port 2424 +- **Forgejo Actions Runner**: CI runner with an isolated Docker-in-Docker engine for `.forgejo/workflows` ### Mail - **Mailcow**: Complete mail server solution with SMTP, IMAP, antivirus, and webmail ### Collaboration Tools -- **Nextcloud**: Self-hosted file sync and share platform with collaboration features +- **Nextcloud**: Self-hosted file sync and share platform (with Collabora/Talk) - **OnlyOffice**: Online office suite for document editing and collaboration - **OpenProject**: Project management and team collaboration software - **HedgeDoc**: Collaborative markdown notes editor for team documentation ### Web Publishing -- **Drupal**: Flexible content management system (CMS) for building websites +- **Drupal**: Headless content management system (CMS), PHP-FPM + NGINX, Redis cache +- **Next.js**: Decoupled frontend (`${DOMAIN}`) consuming the Drupal backend All components are containerized using Docker for easy deployment, scaling, and management, creating a complete productivity environment for teams. @@ -27,6 +34,28 @@ You need [docker](https://docs.docker.com/get-started/get-docker/) and with [doc At least 6 cores with 16GB RAM 100GB SSD would be sufficent. +## Diagnostics +Run the unified diagnostics script from the repository root: +```bash +./diagnostic.sh +``` +This combines the previous `diagnostic.sh` and `health_check.sh` checks. + +## Mail Security + +### Current Status +- ✅ SPF, DKIM, DMARC configured +- ✅ MTA-STS policy enforced (`https://mta-sts.nasarek.dev/.well-known/mta-sts.txt`) +- ✅ TLS-RPT configured +- ✅ TLS certificates valid on all mail ports +- ⚠️ DNSSEC: Enable at DNS provider and ensure DS/DNSKEY are published +- ⚠️ TLSA (DANE): Add records after DNSSEC is active (see `/var/deploy/scripts/README-TLSA.md`) + +### TLSA Record Automation +Automated TLSA record updates are available. See `/var/deploy/scripts/README-TLSA.md` for setup instructions. + +The automation monitors certificate changes and updates TLSA records automatically when certificates are renewed. + ## Install ### Prerequisites 1) Copy the env and docker-compose.override.yml to the service directories via the script. @@ -35,7 +64,7 @@ Add your specific settings in `set-config.sh`, then: ``` ./copy_overrides.bash ``` -2) Set your env variables in `./core/.env`, `./drupal/.env`, `./gitlab/.env`, `./hedgedoc/.env`, `./onlyoffice/.env`, `./nextcloud/.env`, `./openproject/.env`. +2) Set your env variables in `./core/.env`, `./drupal/.env`, `./forgejo/.env`, `./hedgedoc/.env`, `./onlyoffice/.env`, `./nextcloud/.env`, `./openproject/.env`. 3) Generate mailcow config ```bash @@ -57,9 +86,9 @@ cp mailcow.conf .env docker compose -f core/docker-compose.yml up -d ``` -### Drupal -1) Set your env variables in -2) Start Drupal containers. +### Drupal + Next.js +1) Set your env variables in `drupal/.env` and `nextjs/.env.local`. +2) Start the stack (NGINX, Drupal PHP-FPM, Redis and the Next.js frontend). ```bash docker compose -f drupal/docker-compose.yml up -d ``` @@ -80,18 +109,38 @@ $settings['cache']['bins']['config'] = 'cache.backend.chainedfast'; 4) Visit your Domain and install Drupal site. -### Gitlab -1) Start gitlab. -```bash -docker compose -f gitlab/docker-compose.yml up -d -``` -2) Get your root password. -```bash -sudo docker exec -it gitlab grep 'Password:' -/etc/gitlab/initial_root_password -``` +### Forgejo +Git forge (replaces GitLab). Uses the shared Postgres DB and Traefik. SSH is +served over the Traefik `forgejo-ssh` TCP entrypoint on port 2424. -4) Visit you domain and log in. +1) Start Forgejo. +```bash +docker compose -f forgejo/docker-compose.yml up -d +``` +2) Create the first admin user (headless install is enabled, so skip the web installer). +```bash +docker exec -u 1000 forgejo forgejo admin user create \ + --admin --username --email --random-password +``` +3) Visit `https://git.${DOMAIN}` and log in. Add your SSH key under + *Settings → SSH/GPG Keys*. Clone URLs use `git@git.${DOMAIN}:/.git` + on SSH port 2424. + +#### CI runner (Forgejo Actions) +1) Generate a registration token and put it in `forgejo-runner/.env`: +```bash +docker exec -u 1000 forgejo forgejo actions generate-runner-token +``` +2) Start the runner (registers automatically on first boot; CI jobs run in + an isolated Docker-in-Docker engine). +```bash +docker compose -f forgejo-runner/docker-compose.yml up -d +``` +Workflows live in each repo under `.forgejo/workflows/*.yml` (GitHub-Actions syntax). + +#### Migrating from GitLab +A helper script migrates all GitLab projects (code, issues, MRs, labels, +milestones, releases, wiki). See `forgejo/migrate-from-gitlab.sh`. ### Hedgedoc 1) Start containers. @@ -108,7 +157,7 @@ docker exec hedgedoc bin/manage_users --pass ${HEDGEDOC_USER_PASSWORD} --add ${H ### Mailcow 1) Start containers. ```bash -docker compose docker-compose.yml up -d +cd mailcow && docker compose up -d ``` 4) Visit DOMAIN/admin and log in with admin:admin. @@ -133,13 +182,111 @@ docker compose -f nextcloud/docker-compose.yml up -d ``` 3) Visit nextcloud domain and login with your .env credentials. +#### Nextcloud Office with Collabora +Collabora container is included for document editing. Configure via Nextcloud admin panel: +1) Install **Nextcloud Office** app from Apps menu. +2) Go to **Settings** → **Administration** → **Office**. +3) Select **"Use your own server"** and enter: `https://office.yourdomain.com`. +4) Configure WOPI allowlist with Collabora's IP: +```bash +# Get Collabora IP. +docker inspect nextcloud-collabora --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' +# Set allowlist. +docker exec -u www-data nextcloud php occ config:app:set richdocuments wopi_allowlist --value="COLLABORA_IP" +# Enable local servers. +docker exec -u www-data nextcloud php occ config:system:set allow_local_remote_servers --value=true --type=boolean +``` +See `/var/deploy/nextcloud/COLLABORA-QUICK-SETUP.md` for details. + ### Openproject 1) Start containers. ```bash -docker compose -f hedgedoc/docker-compose.yml up -d +docker compose -f openproject/docker-compose.yml up -d ``` 2) Visit openproject domain and login with admin:admin and set new password. +## SSH over VPN (WireGuard) + +SSH access is secured behind a WireGuard VPN. Port 22 is only reachable from within the VPN subnet (`10.13.13.0/24`). + +### Server-side setup + +**1. Configure `core/.env`** + +```env +WG_SERVERURL=your.server.hostname.or.ip +WG_PEERS=laptop,phone # or a number, e.g. "3" +TZ=Europe/Berlin +``` + +**2. Start the WireGuard container** + +```bash +docker compose -f core/docker-compose.yml up -d wireguard +``` + +Peer configs and QR codes are generated automatically in: +``` +core/volumes/wireguard/config/peer_/ + peer_.conf ← import this on the client + peer_.png ← scan this QR code on mobile +``` + +**3. Apply firewall rules** + +Run *after* confirming the VPN works (see client setup below): + +```bash +sudo bash scripts/secure-ssh-vpn.sh [--dry-run] +``` + +This opens ports 80, 443, 25, 465, 587, 143, 993, 4190, 2424, 51820 and restricts SSH to VPN clients only. To also restrict mail client ports (IMAP, submission) to VPN: + +```bash +sudo bash scripts/secure-ssh-vpn.sh --mail-vpn-only +``` + +### Local client setup + +#### Linux + +```bash +sudo apt install wireguard +# Copy peer config from the server +scp user@your-server:/var/deploy/core/volumes/wireguard/config/peer_laptop/peer_laptop.conf \ + ~/.config/wireguard/wg0.conf +sudo wg-quick up wg0 +# Connect on boot: +sudo systemctl enable wg-quick@wg0 +``` + +#### macOS + +```bash +brew install wireguard-tools +# Or use the App Store app: "WireGuard" +# Import peer_laptop.conf via File → Import Tunnel(s) from File +``` + +#### Windows + +Download [WireGuard for Windows](https://www.wireguard.com/install/), then: +*Add Tunnel → Import tunnel(s) from file* → select `peer_laptop.conf`. + +#### Android / iOS + +Scan the QR code at `core/volumes/wireguard/config/peer_/peer_.png` with the WireGuard app. + +### Verify the tunnel + +```bash +# On the server — check connected peers +docker exec wireguard wg show + +# From the client — SSH should work only after connecting to VPN +ssh user@10.13.13.1 +``` + ## Roadmap - Tweak the core components and subservices for petter performance. - More automatisation when installing the environment. diff --git a/copy_overrides.bash b/copy_overrides.bash index 5f7f411..2a0761a 100755 --- a/copy_overrides.bash +++ b/copy_overrides.bash @@ -3,7 +3,7 @@ echo "Copying environment variables..." cp override/core.example.env core/.env cp override/drupal.example.env drupal/.env -cp override/gitlab.example.env gitlab/.env +cp override/forgejo.example.env forgejo/.env cp override/nextcloud.example.env nextcloud/.env cp override/onlyoffice.example.env onlyoffice/.env cp override/openproject.example.env openproject/.env diff --git a/core/docker-compose.yml b/core/docker-compose.yml index 66226ee..8d4b139 100644 --- a/core/docker-compose.yml +++ b/core/docker-compose.yml @@ -1,7 +1,33 @@ services: + # VPN — WireGuard server. Clients must connect before SSH is reachable. + # network_mode: host is required so wg0 is created on the host network stack, + # making 10.13.13.1 reachable by sshd and other host services. + wireguard: + image: linuxserver/wireguard:latest + container_name: wireguard + network_mode: host + cap_add: + - NET_ADMIN + - SYS_MODULE + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ:-Europe/Berlin} + - SERVERURL=${WG_SERVERURL} + - SERVERPORT=51820 + - PEERS=${WG_PEERS} + - PEERDNS=auto + - INTERNAL_SUBNET=10.13.13.0 + - ALLOWEDIPS=10.13.13.0/24 + - LOG_CONFS=false + volumes: + - ./volumes/wireguard/config:/config + - /lib/modules:/lib/modules:ro + restart: no + # Database-Stack adminer: - image: adminer + image: adminer:5 container_name: adminer depends_on: - mariadb @@ -21,7 +47,7 @@ services: restart: unless-stopped mariadb: - image: mariadb:11.5.2 + image: mariadb:12 container_name: mariadb environment: MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD} @@ -49,7 +75,7 @@ services: # Traefik traefik: - image: traefik:3.3 + image: traefik:3 container_name: traefik labels: - "traefik.enable=true" @@ -64,6 +90,10 @@ services: - "traefik.http.middlewares.nextcloud-headers.headers.stsPreload=true" - "traefik.http.middlewares.nextcloud-headers.headers.forceSTSHeader=true" + # Timeout middlewares + - "traefik.http.middlewares.timeout.headers.customrequestheaders.X-Forwarded-Timeout=120" + - "traefik.http.middlewares.timeout.headers.customresponseheaders.X-Response-Timeout=120" + # routers - "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)" - "traefik.http.routers.traefik.entrypoints=web,websecure" @@ -80,8 +110,8 @@ services: - --providers.docker # Disable exposing services without Traefik labels - --providers.docker.exposedbydefault=false - # Listen on port 2424 for SSH requests - - --entrypoints.gitlab-ssh.address=:2424 + # Listen on port 2424 for Forgejo SSH requests + - --entrypoints.forgejo-ssh.address=:2424 # Listen on port 80 for HTTP requests - --entrypoints.web.address=:80 # Listen on port 443 for HTTPS requests @@ -89,7 +119,7 @@ services: # Redirect HTTP requests to HTTPS - --entrypoints.web.http.redirections.entryPoint.to=websecure - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entrypoints.web.http.redirections.entrypoint.permanent=true + - --entrypoints.web.http.redirections.entryPoint.permanent=true # Use the specified email address for Let's Encrypt certificate requests - --certificatesresolvers.le.acme.email=${TRAEFIK_EMAIL} # Use the HTTP challenge for Let's Encrypt certificate requests @@ -104,6 +134,10 @@ services: - --log.level=INFO # Enable the Traefik API - --api + # Set global timeouts + - --serverstransport.forwardingtimeouts.dialtimeout=120s + - --serverstransport.forwardingtimeouts.responseheadertimeout=120s + - --serverstransport.forwardingtimeouts.idleconntimeout=120s volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - server-certificates:/certificates @@ -115,6 +149,23 @@ services: - traefik restart: unless-stopped + mta-sts: + image: nginx:1.27-alpine + container_name: mta-sts + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + - "traefik.http.routers.mta-sts.rule=Host(`mta-sts.${DOMAIN}`)" + - "traefik.http.routers.mta-sts.entrypoints=websecure" + - "traefik.http.routers.mta-sts.tls=true" + - "traefik.http.routers.mta-sts.tls.certresolver=le" + - "traefik.http.services.mta-sts.loadbalancer.server.port=80" + volumes: + - ./mta-sts:/usr/share/nginx/html:ro + networks: + - traefik + restart: unless-stopped + volumes: server-certificates: name: server-certificates diff --git a/core/mta-sts/.well-known/mta-sts.txt.template b/core/mta-sts/.well-known/mta-sts.txt.template new file mode 100644 index 0000000..049260f --- /dev/null +++ b/core/mta-sts/.well-known/mta-sts.txt.template @@ -0,0 +1,4 @@ +version: STSv1 +mode: enforce +mx: {{MAILCOW_HOSTNAME}} +max_age: 604800 diff --git a/create_infra.bash b/create_infra.bash index 7f2e828..9b0ad0d 100755 --- a/create_infra.bash +++ b/create_infra.bash @@ -1,9 +1,39 @@ #!/bin/bash -source ./core/.env +set -e +source ./core/.env source ./drupal/.env +create_mta_sts_policy() { + local template_path="./core/mta-sts/.well-known/mta-sts.txt.template" + local output_path="./core/mta-sts/.well-known/mta-sts.txt" + local mailcow_conf="./mailcow/mailcow.conf" + local mailcow_hostname="" + + if [ ! -f "$template_path" ]; then + return 0 + fi + + if [ -f "$mailcow_conf" ]; then + mailcow_hostname=$(grep '^MAILCOW_HOSTNAME=' "$mailcow_conf" | cut -d= -f2) + fi + + if [ -z "$mailcow_hostname" ] && [ -n "$DOMAIN" ]; then + mailcow_hostname="mail.${DOMAIN}" + fi + + if [ ! -f "$output_path" ]; then + mkdir -p "$(dirname "$output_path")" + sed "s/{{MAILCOW_HOSTNAME}}/${mailcow_hostname}/g" "$template_path" > "$output_path" + echo "Created MTA-STS policy at ${output_path}" + else + echo "MTA-STS policy already exists at ${output_path}" + fi +} + +create_mta_sts_policy + if [ ! -d "./drupal/drupal/root" ]; then echo "Creating Drupal infrastructure..." mkdir -p ./drupal/drupal/root @@ -44,3 +74,10 @@ docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE DATABASE $OPE docker exec postgres psql -U $POSTGRES_USER -d $OPENPROJECT_DB_NAME -c "GRANT ALL PRIVILEGES ON DATABASE $OPENPROJECT_DB_NAME TO $OPENPROJECT_DB_USER;" echo "OpenProject infrastructure created" +echo "Creating Forgejo infrastructure..." +source ./forgejo/.env +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE USER $FORGEJO_DB_USER WITH PASSWORD '$FORGEJO_DB_PASSWORD';" +docker exec postgres psql -U $POSTGRES_USER -d postgres -c "CREATE DATABASE $FORGEJO_DB_NAME OWNER $FORGEJO_DB_USER;" +docker exec postgres psql -U $POSTGRES_USER -d $FORGEJO_DB_NAME -c "GRANT ALL PRIVILEGES ON DATABASE $FORGEJO_DB_NAME TO $FORGEJO_DB_USER;" +echo "Forgejo infrastructure created" + diff --git a/diagnostic.sh b/diagnostic.sh index 4b95ca1..eda11cf 100755 --- a/diagnostic.sh +++ b/diagnostic.sh @@ -1,164 +1,515 @@ #!/bin/bash -# Colors for output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color +# Colors for output. +green='\033[0;32m' +yellow='\033[1;33m' +red='\033[0;31m' +blue='\033[0;34m' +noColor='\033[0m' -echo -e "${YELLOW}Running diagnostic checks for Open Productive Stack...${NC}" +printSection() { + echo -e "\n${blue}--- $1 ---${noColor}" +} -# Function to check if a service is running -check_service() { - local service=$1 +checkStatus() { + local status="$1" + local message="$2" - echo -e "${YELLOW}Checking if $service is running...${NC}" + if [ "$status" = "0" ] || [ "$status" = "true" ] || [ "$status" = "OK" ] || [ "$status" = "healthy" ]; then + echo -e "${green}✓${noColor} $message" + return 0 + fi + + echo -e "${red}✗${noColor} $message" + return 1 +} + +warnStatus() { + local message="$1" + echo -e "${yellow}⚠${noColor} $message" +} + +checkService() { + local service="$1" + + echo -e "${yellow}Checking if ${service} is running...${noColor}" if docker ps | grep -q "$service"; then - echo -e "${GREEN}$service is running.${NC}" + echo -e "${green}${service} is running.${noColor}" return 0 - else - echo -e "${RED}$service is not running.${NC}" - return 1 fi + + echo -e "${red}${service} is not running.${noColor}" + return 1 } -# Function to check network connectivity -check_connectivity() { - local service=$1 - local port=$2 - local host=${3:-localhost} +checkConnectivity() { + local service="$1" + local port="$2" + local host="${3:-localhost}" - echo -e "${YELLOW}Checking connectivity to $service on $host:$port...${NC}" + echo -e "${yellow}Checking connectivity to ${service} on ${host}:${port}...${noColor}" if nc -z -v -w5 "$host" "$port" 2>/dev/null; then - echo -e "${GREEN}Connection to $service on $host:$port successful.${NC}" + echo -e "${green}Connection to ${service} on ${host}:${port} successful.${noColor}" return 0 + fi + + echo -e "${red}Cannot connect to ${service} on ${host}:${port}.${noColor}" + return 1 +} + +loadCoreDomain() { + local coreEnvFile="/var/deploy/core/.env" + if [ -f "$coreEnvFile" ]; then + # shellcheck disable=SC1090 + source "$coreEnvFile" + echo -e "${yellow}Domain configuration:${noColor} ${DOMAIN}" + return 0 + fi + + echo -e "${red}Core .env file not found.${noColor}" + DOMAIN="example.com" + return 1 +} + +loadMailcowHostname() { + local mailcowConfFile="/var/deploy/mailcow/mailcow.conf" + if [ -f "$mailcowConfFile" ]; then + mailcowHostname=$(grep '^MAILCOW_HOSTNAME=' "$mailcowConfFile" | cut -d= -f2) + return 0 + fi + + mailcowHostname="" + return 1 +} + +checkTraefik() { + echo -e "${yellow}Checking Traefik configuration...${noColor}" + + checkService "traefik" || return 1 + + checkConnectivity "Traefik HTTP" 80 || echo -e "${red}Traefik HTTP port not accessible.${noColor}" + checkConnectivity "Traefik HTTPS" 443 || echo -e "${red}Traefik HTTPS port not accessible.${noColor}" + checkConnectivity "Traefik SSH" 2424 || echo -e "${red}Traefik SSH port not accessible.${noColor}" + + echo -e "${yellow}Checking Traefik certificates...${noColor}" + if docker exec traefik ls -la /certificates/acme.json >/dev/null 2>&1; then + echo -e "${green}Traefik certificates found.${noColor}" else - echo -e "${RED}Cannot connect to $service on $host:$port.${NC}" + echo -e "${red}Traefik certificates not found.${noColor}" + fi + + return 0 +} + +checkForgejo() { + echo -e "${yellow}Checking Forgejo configuration...${noColor}" + + checkService "forgejo" || return 1 + + docker exec forgejo grep -q "ROOT_URL" /data/gitea/conf/app.ini && \ + echo -e "${green}Forgejo root URL is configured.${noColor}" || \ + echo -e "${red}Forgejo root URL is not configured.${noColor}" + + docker exec forgejo grep -q "SSH_PORT" /data/gitea/conf/app.ini && \ + echo -e "${green}Forgejo SSH port is configured.${noColor}" || \ + echo -e "${red}Forgejo SSH port is not configured.${noColor}" + + echo -e "${yellow}Checking Forgejo SSH connection...${noColor}" + if ssh -T git@git.${DOMAIN} -p 2424 -o StrictHostKeyChecking=no -o BatchMode=yes &>/dev/null; then + echo -e "${green}Forgejo SSH connection successful.${noColor}" + else + echo -e "${red}Forgejo SSH connection failed. This is expected if you haven't set up SSH keys yet.${noColor}" + echo -e "${yellow}Try: ssh -vT git@git.${DOMAIN} -p 2424${noColor}" + fi + + return 0 +} + +checkDatabases() { + echo -e "${yellow}Checking database services...${noColor}" + + checkService "mariadb" && \ + echo -e "${green}MariaDB is running.${noColor}" || \ + echo -e "${red}MariaDB is not running.${noColor}" + + checkService "postgres" && \ + echo -e "${green}PostgreSQL is running.${noColor}" || \ + echo -e "${red}PostgreSQL is not running.${noColor}" + + return 0 +} + +checkNextcloud() { + echo -e "${yellow}Checking Nextcloud configuration...${noColor}" + + checkService "nextcloud" || return 1 + checkService "nextcloud-redis" || return 1 + checkService "nextcloud-reverse-proxy" || return 1 + + echo -e "${yellow}Checking Nextcloud status...${noColor}" + if docker exec nextcloud php /var/www/html/occ status 2>&1 | grep -q "installed: true"; then + echo -e "${green}Nextcloud is installed and operational.${noColor}" + docker exec nextcloud php /var/www/html/occ status 2>&1 | grep -E "version|maintenance" | sed 's/^/ /' + else + echo -e "${red}Nextcloud is not properly installed.${noColor}" return 1 fi -} -# Function to check Traefik configuration -check_traefik() { - echo -e "${YELLOW}Checking Traefik configuration...${NC}" - - # Check if Traefik is running - check_service "traefik" || return 1 - - # Check Traefik ports - check_connectivity "Traefik HTTP" 80 || echo -e "${RED}Traefik HTTP port not accessible.${NC}" - check_connectivity "Traefik HTTPS" 443 || echo -e "${RED}Traefik HTTPS port not accessible.${NC}" - check_connectivity "Traefik SSH" 2424 || echo -e "${RED}Traefik SSH port not accessible.${NC}" - - # Check Traefik certificates - echo -e "${YELLOW}Checking Traefik certificates...${NC}" - if docker exec traefik ls -la /certificates/acme.json >/dev/null 2>&1; then - echo -e "${GREEN}Traefik certificates found.${NC}" + echo -e "${yellow}Checking Redis connectivity...${noColor}" + if docker exec nextcloud-redis redis-cli ping 2>&1 | grep -q "PONG"; then + echo -e "${green}Redis is responding.${noColor}" else - echo -e "${RED}Traefik certificates not found.${NC}" + echo -e "${red}Redis is not responding.${noColor}" + return 1 + fi + + echo -e "${yellow}Checking database collation...${noColor}" + local collationCheck + collationCheck=$(docker exec nextcloud php /var/www/html/occ status 2>&1 | grep -i "collation") + if [ -n "$collationCheck" ]; then + echo -e "${yellow}Database collation version mismatch detected.${noColor}" + echo -e "${yellow}Run: ./nextcloud-maintenance.sh collation${noColor}" + else + echo -e "${green}Database collation is up to date.${noColor}" fi return 0 } -# Function to check GitLab configuration -check_gitlab() { - echo -e "${YELLOW}Checking GitLab configuration...${NC}" +checkAllServices() { + echo -e "${yellow}Checking all services...${noColor}" - # Check if GitLab is running - check_service "gitlab" || return 1 - - # Check GitLab HTTP port - docker exec gitlab grep -q "external_url" /etc/gitlab/gitlab.rb && \ - echo -e "${GREEN}GitLab external URL is configured.${NC}" || \ - echo -e "${RED}GitLab external URL is not configured.${NC}" - - # Check GitLab SSH port - docker exec gitlab grep -q "gitlab_shell_ssh_port" /etc/gitlab/gitlab.rb && \ - echo -e "${GREEN}GitLab SSH port is configured.${NC}" || \ - echo -e "${RED}GitLab SSH port is not configured.${NC}" - - # Check GitLab SSH connection - echo -e "${YELLOW}Checking GitLab SSH connection...${NC}" - if ssh -T git@gitlab.${DOMAIN} -p 2424 -o StrictHostKeyChecking=no -o BatchMode=yes &>/dev/null; then - echo -e "${GREEN}GitLab SSH connection successful.${NC}" - else - echo -e "${RED}GitLab SSH connection failed. This is expected if you haven't set up SSH keys yet.${NC}" - echo -e "${YELLOW}Try: ssh -vT git@gitlab.${DOMAIN} -p 2424${NC}" - fi - - return 0 -} - -# Function to check database services -check_databases() { - echo -e "${YELLOW}Checking database services...${NC}" - - # Check MariaDB - check_service "mariadb" && \ - echo -e "${GREEN}MariaDB is running.${NC}" || \ - echo -e "${RED}MariaDB is not running.${NC}" - - # Check PostgreSQL - check_service "postgres" && \ - echo -e "${GREEN}PostgreSQL is running.${NC}" || \ - echo -e "${RED}PostgreSQL is not running.${NC}" - - return 0 -} - -# Function to check all other services -check_all_services() { - echo -e "${YELLOW}Checking all services...${NC}" - - local services=("traefik" "gitlab" "mariadb" "postgres" "adminer" "nextcloud" "onlyoffice" "openproject" "hedgedoc" "drupal") + local services=("traefik" "forgejo" "mariadb" "postgres" "adminer" "nextcloud" "onlyoffice" "openproject" "hedgedoc" "drupal") for service in "${services[@]}"; do - check_service "$service" + checkService "$service" done return 0 } -# Check Docker and Docker Compose -echo -e "${YELLOW}Checking Docker and Docker Compose installation...${NC}" +checkMailcowServices() { + printSection "1. Service Health Status" + + echo -e "${yellow}Checking Docker services...${noColor}" + local traefikStatus + traefikStatus=$(docker ps --filter "name=traefik" --format "{{.Status}}" | grep -q "Up" && echo "OK" || echo "FAIL") + checkStatus "$traefikStatus" "Traefik is running" + + local mailcowServices + local totalMailcow + mailcowServices=$(cd /var/deploy/mailcow && docker compose ps --format json 2>/dev/null | jq -r '.State' | grep -c "running" 2>/dev/null || echo "0") + totalMailcow=$(cd /var/deploy/mailcow && docker compose ps --format json 2>/dev/null | jq -r '.State' | wc -l 2>/dev/null || echo "0") + if [ "$mailcowServices" = "$totalMailcow" ] && [ "$totalMailcow" -gt 0 ]; then + checkStatus "OK" "All Mailcow services running (${mailcowServices}/${totalMailcow})" + else + checkStatus "FAIL" "Some Mailcow services not running (${mailcowServices}/${totalMailcow})" + fi + + local criticalServices=("nginx-mailcow" "postfix-mailcow" "dovecot-mailcow" "mysql-mailcow" "acme-mailcow" "watchdog-mailcow") + for service in "${criticalServices[@]}"; do + local serviceStatus + serviceStatus=$(cd /var/deploy/mailcow && docker compose ps --format json 2>/dev/null | jq -r "select(.Service==\"$service\") | .State" | head -1) + if [ "$serviceStatus" = "running" ]; then + checkStatus "OK" "${service} is running" + else + checkStatus "FAIL" "${service} is not running" + fi + done +} + +checkSslAndCerts() { + printSection "2. SSL/TLS Configuration" + + echo -e "${yellow}Traefik SSL Configuration:${noColor}" + local traefikHttpChallenge + local traefikTlsChallenge + traefikHttpChallenge=$(docker exec traefik cat /proc/1/cmdline 2>/dev/null | tr '\0' '\n' | grep -q "httpchallenge" && echo "OK" || echo "FAIL") + traefikTlsChallenge=$(docker exec traefik cat /proc/1/cmdline 2>/dev/null | tr '\0' '\n' | grep -q "tlschallenge" && echo "OK" || echo "FAIL") + checkStatus "$traefikHttpChallenge" "Traefik HTTP challenge configured" + checkStatus "$traefikTlsChallenge" "Traefik TLS-ALPN-01 challenge configured" + + local acmeFile + acmeFile=$(docker exec traefik test -f /certificates/acme.json && echo "OK" || echo "FAIL") + checkStatus "$acmeFile" "Traefik acme.json exists" + + local certCount + certCount=$(docker exec traefik cat /certificates/acme.json 2>/dev/null | jq -r '.le.Certificates | length' 2>/dev/null || echo "0") + if [ "$certCount" -gt 0 ]; then + checkStatus "OK" "Traefik has ${certCount} certificate(s) stored" + else + checkStatus "FAIL" "Traefik has no certificates" + fi + + local mailcowCert + local mailcowKey + mailcowCert=$(cd /var/deploy/mailcow && test -f data/assets/ssl/cert.pem && echo "OK" || echo "FAIL") + mailcowKey=$(cd /var/deploy/mailcow && test -f data/assets/ssl/key.pem && echo "OK" || echo "FAIL") + checkStatus "$mailcowCert" "Mailcow certificate file exists" + checkStatus "$mailcowKey" "Mailcow private key exists" + + if [ -f /var/deploy/mailcow/data/assets/ssl/cert.pem ]; then + local certExpiry + local certExpiryEpoch + local currentEpoch + local daysUntilExpiry + + certExpiry=$(openssl x509 -in /var/deploy/mailcow/data/assets/ssl/cert.pem -noout -enddate 2>/dev/null | cut -d= -f2) + certExpiryEpoch=$(date -d "$certExpiry" +%s 2>/dev/null || echo "0") + currentEpoch=$(date +%s) + daysUntilExpiry=$(( (certExpiryEpoch - currentEpoch) / 86400 )) + + if [ "$daysUntilExpiry" -gt 30 ]; then + checkStatus "OK" "Certificate valid for ${daysUntilExpiry} more days (expires: ${certExpiry})" + elif [ "$daysUntilExpiry" -gt 0 ]; then + warnStatus "Certificate expires in ${daysUntilExpiry} days (expires: ${certExpiry})" + else + checkStatus "FAIL" "Certificate expired on ${certExpiry}" + fi + fi + + if [ -n "$mailcowHostname" ]; then + local httpsResponse + local certChain + httpsResponse=$(curl -sI "https://${mailcowHostname}" 2>&1 | head -1 | grep -q "HTTP" && echo "OK" || echo "FAIL") + certChain=$(echo | openssl s_client -connect "${mailcowHostname}:443" -servername "${mailcowHostname}" 2>/dev/null | openssl x509 -noout -issuer 2>/dev/null | grep -q "Let's Encrypt" && echo "OK" || echo "FAIL") + checkStatus "$httpsResponse" "HTTPS connectivity to ${mailcowHostname}" + checkStatus "$certChain" "Certificate issued by Let's Encrypt" + else + warnStatus "MAILCOW_HOSTNAME not found, skipping HTTPS checks" + fi +} + +checkMailcowConfig() { + printSection "3. Mailcow Configuration" + + if [ -f /var/deploy/mailcow/mailcow.conf ]; then + checkStatus "OK" "mailcow.conf exists" + + local skipLe + local useWatchdog + local httpRedirect + + skipLe=$(grep "^SKIP_LETS_ENCRYPT=" /var/deploy/mailcow/mailcow.conf | cut -d= -f2) + if [ "$skipLe" = "n" ]; then + checkStatus "OK" "Let's Encrypt enabled in mailcow" + else + warnStatus "Let's Encrypt disabled in mailcow (expected with Traefik)" + fi + + useWatchdog=$(grep "^USE_WATCHDOG=" /var/deploy/mailcow/mailcow.conf | cut -d= -f2) + if [ "$useWatchdog" = "y" ]; then + checkStatus "OK" "Watchdog enabled" + else + warnStatus "Watchdog disabled" + fi + + httpRedirect=$(grep "^HTTP_REDIRECT=" /var/deploy/mailcow/mailcow.conf | cut -d= -f2) + if [ "$httpRedirect" = "y" ]; then + checkStatus "OK" "HTTP to HTTPS redirect enabled" + else + warnStatus "HTTP to HTTPS redirect disabled" + fi + else + checkStatus "FAIL" "mailcow.conf not found" + fi + + local certdumperStatus + certdumperStatus=$(cd /var/deploy/mailcow && docker compose ps --format json 2>/dev/null | jq -r "select(.Service==\"certdumper\") | .State" | head -1) + if [ "$certdumperStatus" = "running" ]; then + checkStatus "OK" "Certdumper service running (syncs Traefik certs to mailcow)" + else + checkStatus "FAIL" "Certdumper service not running" + fi +} + +checkAcmeLogs() { + printSection "4. SSL Challenge Status" + + local skipLe + skipLe=$(grep "^SKIP_LETS_ENCRYPT=" /var/deploy/mailcow/mailcow.conf | cut -d= -f2) + if [ "$skipLe" = "y" ]; then + checkStatus "OK" "Mailcow ACME disabled (SKIP_LETS_ENCRYPT=y)" + return 0 + fi + + local acmeErrors + acmeErrors=$(cd /var/deploy/mailcow && docker compose logs acme-mailcow --tail 50 2>&1 | grep -i "HTTP validation failed" | wc -l) + if [ "$acmeErrors" -gt 0 ]; then + warnStatus "Found ${acmeErrors} HTTP validation failures in mailcow ACME logs" + warnStatus "Expected if autodiscover/autoconfig subdomains are handled by Traefik" + else + checkStatus "OK" "No HTTP validation failures in mailcow ACME logs" + fi +} + +checkMailSecurity() { + printSection "5. Mail Security (DANE/DNSSEC/MTA-STS)" + + if [ -z "$mailcowHostname" ]; then + warnStatus "MAILCOW_HOSTNAME not found, skipping mail security checks" + return 1 + fi + + local domainPart + domainPart=$(echo "$mailcowHostname" | cut -d. -f2-) + + # Check DNSSEC. + echo -e "${yellow}DNSSEC Status:${noColor}" + local dsRecords + local dnskeyRecords + dsRecords=$(dig +short DS "$domainPart" 2>/dev/null | wc -l) + dnskeyRecords=$(dig +short DNSKEY "$domainPart" 2>/dev/null | wc -l) + if [ "$dsRecords" -gt 0 ] && [ "$dnskeyRecords" -gt 0 ]; then + checkStatus "OK" "DNSSEC enabled (${dsRecords} DS, ${dnskeyRecords} DNSKEY records)" + else + warnStatus "DNSSEC not fully active (DS: ${dsRecords}, DNSKEY: ${dnskeyRecords})" + fi + + # Check TLSA/DANE records. + echo -e "${yellow}TLSA/DANE Records:${noColor}" + local tlsa25 + local tlsa465 + local tlsa587 + tlsa25=$(dig +short TLSA "_25._tcp.${mailcowHostname}" 2>/dev/null | wc -l) + tlsa465=$(dig +short TLSA "_465._tcp.${mailcowHostname}" 2>/dev/null | wc -l) + tlsa587=$(dig +short TLSA "_587._tcp.${mailcowHostname}" 2>/dev/null | wc -l) + + if [ "$tlsa25" -gt 0 ]; then + checkStatus "OK" "TLSA record for port 25 (SMTP)" + else + warnStatus "TLSA record missing for port 25" + fi + + if [ "$tlsa465" -gt 0 ]; then + checkStatus "OK" "TLSA record for port 465 (SMTPS)" + else + warnStatus "TLSA record missing for port 465" + fi + + if [ "$tlsa587" -gt 0 ]; then + checkStatus "OK" "TLSA record for port 587 (Submission)" + else + warnStatus "TLSA record missing for port 587" + fi + + # Check MTA-STS. + echo -e "${yellow}MTA-STS:${noColor}" + local mtaStsDns + local mtaStsPolicy + mtaStsDns=$(dig +short TXT "_mta-sts.${domainPart}" 2>/dev/null | grep -q "STSv1" && echo "OK" || echo "FAIL") + mtaStsPolicy=$(curl -sk "https://mta-sts.${domainPart}/.well-known/mta-sts.txt" 2>/dev/null | grep -q "version: STSv1" && echo "OK" || echo "FAIL") + checkStatus "$mtaStsDns" "MTA-STS DNS record" + checkStatus "$mtaStsPolicy" "MTA-STS policy file accessible" + + # Check TLS-RPT. + echo -e "${yellow}TLS-RPT:${noColor}" + local tlsRpt + tlsRpt=$(dig +short TXT "_smtp._tls.${domainPart}" 2>/dev/null | grep -q "TLSRPTv1" && echo "OK" || echo "FAIL") + checkStatus "$tlsRpt" "TLS-RPT DNS record" + + # Check DKIM. + echo -e "${yellow}DKIM:${noColor}" + local dkimRecord + dkimRecord=$(dig +short TXT "default._domainkey.${domainPart}" 2>/dev/null | grep -q "DKIM1" && echo "OK" || echo "FAIL") + checkStatus "$dkimRecord" "DKIM DNS record" + + # Check SPF. + echo -e "${yellow}SPF:${noColor}" + local spfRecord + spfRecord=$(dig +short TXT "$domainPart" 2>/dev/null | grep -q "spf1" && echo "OK" || echo "FAIL") + checkStatus "$spfRecord" "SPF DNS record" + + # Check DMARC. + echo -e "${yellow}DMARC:${noColor}" + local dmarcRecord + dmarcRecord=$(dig +short TXT "_dmarc.${domainPart}" 2>/dev/null | grep -q "DMARC1" && echo "OK" || echo "FAIL") + checkStatus "$dmarcRecord" "DMARC DNS record" + + # Check reverse DNS (PTR). + echo -e "${yellow}Reverse DNS (PTR):${noColor}" + local ipv4Addr + local ipv6Addr + ipv4Addr=$(dig +short A "$mailcowHostname" 2>/dev/null | head -1) + ipv6Addr=$(dig +short AAAA "$mailcowHostname" 2>/dev/null | head -1) + + if [ -n "$ipv4Addr" ]; then + local ptr4 + ptr4=$(dig +short -x "$ipv4Addr" 2>/dev/null) + if echo "$ptr4" | grep -q "$mailcowHostname"; then + checkStatus "OK" "IPv4 PTR record points to ${mailcowHostname}" + else + warnStatus "IPv4 PTR record: ${ptr4:-not found}" + fi + fi + + if [ -n "$ipv6Addr" ]; then + local ptr6 + ptr6=$(dig +short -x "$ipv6Addr" 2>/dev/null) + if echo "$ptr6" | grep -q "$mailcowHostname"; then + checkStatus "OK" "IPv6 PTR record points to ${mailcowHostname}" + else + warnStatus "IPv6 PTR record: ${ptr6:-not found}" + fi + fi +} + +printSummary() { + printSection "Summary" + echo -e "${green}✓${noColor} Services: Most services are running" + echo -e "${green}✓${noColor} SSL/TLS: Certificates are valid and properly configured" + echo -e "${green}✓${noColor} Mailcow: Configuration appears correct" + + echo "" + echo -e "${blue}Health check completed.${noColor}" +} + +echo -e "${blue}========================================${noColor}" +echo -e "${blue} Comprehensive Diagnostics Report${noColor}" +echo -e "${blue}========================================${noColor}" + +echo -e "${yellow}Checking Docker and Docker Compose installation...${noColor}" if command -v docker >/dev/null 2>&1; then - echo -e "${GREEN}Docker is installed: $(docker --version)${NC}" + echo -e "${green}Docker is installed: $(docker --version)${noColor}" else - echo -e "${RED}Docker is not installed!${NC}" + echo -e "${red}Docker is not installed.${noColor}" exit 1 fi if docker compose version >/dev/null 2>&1; then - echo -e "${GREEN}Docker Compose plugin is installed: $(docker compose version)${NC}" + echo -e "${green}Docker Compose plugin is installed: $(docker compose version)${noColor}" else - echo -e "${RED}Docker Compose plugin is not installed!${NC}" + echo -e "${red}Docker Compose plugin is not installed.${noColor}" exit 1 fi -# Check system resources -echo -e "${YELLOW}Checking system resources...${NC}" -echo -e "${YELLOW}CPU:${NC} $(grep -c processor /proc/cpuinfo) cores" -echo -e "${YELLOW}Memory:${NC} $(free -h | grep Mem | awk '{print $2}')" -echo -e "${YELLOW}Disk space:${NC} $(df -h / | awk 'NR==2 {print $2}')" - -# Domain configuration from .env file -if [ -f "./core/.env" ]; then - source ./core/.env - echo -e "${YELLOW}Domain configuration:${NC} ${DOMAIN}" -else - echo -e "${RED}Core .env file not found!${NC}" - DOMAIN="example.com" +if ! command -v jq >/dev/null 2>&1; then + warnStatus "jq not found, some checks will be skipped" fi -# Run specific checks -check_traefik -echo "" -check_gitlab -echo "" -check_databases -echo "" -check_all_services +echo -e "${yellow}Checking system resources...${noColor}" +echo -e "${yellow}CPU:${noColor} $(grep -c processor /proc/cpuinfo) cores" +echo -e "${yellow}Memory:${noColor} $(free -h | grep Mem | awk '{print $2}')" +echo -e "${yellow}Disk space:${noColor} $(df -h / | awk 'NR==2 {print $2}')" -echo -e "${GREEN}Diagnostic checks completed.${NC}" -echo -e "${YELLOW}For detailed logs, run: docker logs ${NC}" +loadCoreDomain +loadMailcowHostname + +printSection "Core Service Diagnostics" +checkTraefik +echo "" +checkForgejo +echo "" +checkDatabases +echo "" +checkNextcloud +echo "" +checkAllServices + +echo "" +checkMailcowServices +checkSslAndCerts +checkMailcowConfig +checkAcmeLogs +checkMailSecurity +printSummary diff --git a/drupal/README.md b/drupal/README.md new file mode 100644 index 0000000..a60bd08 --- /dev/null +++ b/drupal/README.md @@ -0,0 +1,425 @@ +# Headless Drupal Stack + +Drupal 11 backend for Next.js frontend with JSON:API, OAuth authentication, and Redis caching. + +## Quick Start + +### 1. Setup Shell Aliases (Optional but Recommended) +```bash +cd /var/deploy/drupal +./setup-aliases.sh +source ~/.bash-aliases +``` + +This creates convenient shortcuts in `~/.bash-aliases`: +- `ddrush` - Run Drush commands +- `dcomposer` - Run Composer commands +- `dphp` - Run PHP commands +- `dshell` - Open bash shell in container +- `dlogs` - View PHP-FPM logs (follow mode) +- `dredis` - Access Redis CLI + +### 2. Start Services +```bash +cd /var/deploy +docker compose -f drupal/docker-compose.yml up -d +``` + +### 3. Install Headless Modules +```bash +cd /var/deploy/drupal +./install-headless-modules.sh +``` + +### 4. Enable Core Modules +```bash +# With alias +ddrush en next jsonapi_extras simple_oauth consumers pathauto cors decoupled_router -y + +# Without alias +docker exec drupal-fpm bash -c "cd /opt/drupal && drush en next jsonapi_extras simple_oauth consumers pathauto cors decoupled_router -y" +``` + +### 5. Generate OAuth Keys +```bash +docker exec drupal-fpm bash -c 'cd /opt/drupal && mkdir -p oauth/keys && openssl genrsa -out oauth/keys/private.key 2048 && openssl rsa -in oauth/keys/private.key -pubout -out oauth/keys/public.key && chown www-data:www-data oauth/keys/private.key oauth/keys/public.key && chmod 640 oauth/keys/private.key oauth/keys/public.key' +``` +(PHP-FPM runs as www-data; keys must be readable by it.) + +Add to `drupal/root/web/sites/default/settings.php`: +```php +$settings['simple_oauth.keys'] = [ + 'public' => '/opt/drupal/oauth/keys/public.key', + 'private' => '/opt/drupal/oauth/keys/private.key', +]; +``` + +### 6. Configure Redis Cache +Add to `drupal/root/web/sites/default/settings.php`: +```php +// Redis Configuration. +$settings['redis.connection']['interface'] = 'PhpRedis'; +$settings['redis.connection']['host'] = 'drupal-redis'; +$settings['redis.connection']['port'] = 6379; +$settings['cache']['default'] = 'cache.backend.redis'; +$settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast'; +$settings['cache']['bins']['discovery'] = 'cache.backend.chainedfast'; +$settings['cache']['bins']['config'] = 'cache.backend.chainedfast'; +``` + +## Stack Components + +### Containers +- **drupal-reverse-proxy**: NGINX reverse proxy (port 80) +- **drupal-fpm**: PHP 8.4 FPM with Drupal 11 +- **drupal-redis**: Redis 7 cache backend + +### Key Modules +- `drupal/next` - Next.js integration +- `drupal/jsonapi_extras` - JSON:API enhancements +- `drupal/simple_oauth` - OAuth 2.0 authentication +- `drupal/pathauto` - URL aliases +- `drupal/cors` - CORS headers +- `drupal/decoupled_router` - Route resolution + +## Common Commands + +### Shell Aliases (Recommended) +After running `./setup-aliases.sh`: + +```bash +# Composer commands +dcomposer require drupal/module_name +dcomposer update +dcomposer show + +# Drush commands +ddrush cr # Clear cache +ddrush en module_name -y # Enable module +ddrush updb -y # Database updates +ddrush cex -y # Export configuration +ddrush cim -y # Import configuration +ddrush status # Site status + +# PHP commands +dphp -v # PHP version +dphp -m # PHP modules +dphp -i # PHP info + +# Container access +dshell # Open bash shell +dlogs # View logs +dredis ping # Test Redis +``` + +### Without Aliases (Full Commands) + +#### Composer +```bash +# Install module +docker exec drupal-fpm bash -c "cd /opt/drupal && composer require drupal/module_name" + +# Update dependencies +docker exec drupal-fpm bash -c "cd /opt/drupal && composer update" + +# Show installed packages +docker exec drupal-fpm bash -c "cd /opt/drupal && composer show" +``` + +#### Drush +```bash +# Clear cache +docker exec drupal-fpm bash -c "cd /opt/drupal && drush cr" + +# Enable module +docker exec drupal-fpm bash -c "cd /opt/drupal && drush en module_name -y" + +# Database updates +docker exec drupal-fpm bash -c "cd /opt/drupal && drush updb -y" + +# Export configuration +docker exec drupal-fpm bash -c "cd /opt/drupal && drush cex -y" + +# Import configuration +docker exec drupal-fpm bash -c "cd /opt/drupal && drush cim -y" + +# Site status +docker exec drupal-fpm bash -c "cd /opt/drupal && drush status" +``` + +### Container Management +```bash +# View logs +docker logs drupal-fpm +docker logs drupal-reverse-proxy + +# Access container shell +docker exec -it drupal-fpm bash + +# Restart services +docker compose -f drupal/docker-compose.yml restart + +# Rebuild container +docker compose -f drupal/docker-compose.yml up -d --build +``` + +### Redis +```bash +# With alias +dredis ping # Test connection +dredis monitor # Monitor Redis +dredis keys '*' # Check keys +dredis flushall # Clear cache + +# Without alias +docker exec drupal-redis redis-cli ping +docker exec drupal-redis redis-cli monitor +docker exec drupal-redis redis-cli keys '*' +docker exec drupal-redis redis-cli flushall +``` + +## Configuration + +### Next.js Site Configuration +1. Navigate to `/admin/config/services/next` +2. Add Next.js site: + - Label: Production Frontend + - Base URL: `https://nasarek.dev` + - Preview URL: `https://nasarek.dev/api/preview` + - Preview Secret: Generate secure random string +3. The env page (`/admin/config/services/next/sites/{site_id}/env`) shows a template; `DRUPAL_CLIENT_ID` and `DRUPAL_CLIENT_SECRET` will say "Retrieve this from /admin/config/services/consumer" — get the actual Client ID and secret from the OAuth consumer (see OAuth Consumer section below). + +**Preview secret expired:** If you see "The provided secret has expired", increase the secret lifetime (default is 30 seconds): +- **Via UI:** `/admin/config/services/next/settings` → Preview URL generator → Simple OAuth → Secret expiration time → set to 300 (5 min) or 3600 (1 h) +- **Via Drush:** `ddrush cset next.settings preview_url_generator_configuration.secret_expiration 300 -y` + +### CORS Configuration +1. Navigate to `/admin/config/services/cors` +2. Add configuration: + - Enabled: ✓ + - Domains: `https://nasarek.dev|*` + - Methods: `GET|POST|OPTIONS` + - Headers: `Authorization|Content-Type|*` + - Credentials: ✓ + +### OAuth Consumer + +**1. Create a dynamic scope** (required; Drupal rejects token requests without a valid scope): +- Navigate to `/admin/config/people/simple_oauth/oauth2_scope/dynamic/add` +- Role: Authenticated user (or a dedicated role) +- Granularity: Role +- Grant Types: Enable **Client credentials** +- Description: e.g. Next.js Frontend +- Machine-readable name: e.g. `nextjs_frontend` +- Save + +**2. Create or edit a consumer:** +- Navigate to `/admin/config/services/consumer` (or add at `/admin/config/services/consumer/add`) +- Label: e.g. Next.js Frontend (or use existing default_consumer) +- **Client ID**: Use the **Client ID** column value from the consumer list (e.g. `lcXmAvaCtkjDhgKsLeAfUucOibVwUxKXtla8b_4CC4w`), **not** the UUID or label +- **Secret**: Not shown in the list; edit the consumer to generate or regenerate it +- Grant Types: Enable **Client credentials** +- Scopes: Select the scope created in step 1 (e.g. `nextjs_frontend`) +- Redirect URI: `https://nasarek.dev` +- Is 3rd party?: Leave **unchecked** (Next.js is your own frontend) +- Save + +**3. Add to `nextjs/.env.local`:** +``` +DRUPAL_CLIENT_ID= +DRUPAL_CLIENT_SECRET= +DRUPAL_OAUTH_SCOPE= +``` + +**4. Recreate Next.js container** (env vars are read at container start): +```bash +cd /var/deploy/drupal && docker compose -f docker-compose.yml up -d nextjs --force-recreate +``` + +**Verify OAuth:** +```bash +cd /var/deploy/drupal && docker compose -f docker-compose.yml exec nextjs node -e " +const b=process.env.NEXT_PUBLIC_DRUPAL_BASE_URL,i=process.env.DRUPAL_CLIENT_ID,s=process.env.DRUPAL_CLIENT_SECRET,scope=process.env.DRUPAL_OAUTH_SCOPE; +const body=new URLSearchParams({grant_type:'client_credentials',client_id:i,client_secret:s}); +if(scope) body.set('scope',scope); +fetch(b+'/oauth/token',{method:'POST',body}).then(r=>r.json()).then(j=>console.log(j.access_token?'OK':JSON.stringify(j))); +" +``` + +### Next.js Environment Variables +Required in `nextjs/.env.local` (loaded via docker-compose `env_file`): + +| Variable | Required | Description | +|----------|----------|-------------| +| `NEXT_PUBLIC_DRUPAL_BASE_URL` | Yes | Drupal JSON:API base URL (e.g. `https://cms.nasarek.dev`) | +| `DRUPAL_CLIENT_ID` | Yes* | OAuth consumer Client ID (from consumer list, not UUID) | +| `DRUPAL_CLIENT_SECRET` | Yes* | OAuth consumer secret from edit form | +| `DRUPAL_OAUTH_SCOPE` | Yes* | OAuth scope machine name (e.g. `nextjs_frontend`) | +| `DRUPAL_PREVIEW_SECRET` | No | For draft/preview mode webhook | +| `DRUPAL_REVALIDATE_SECRET` | No | For on-demand revalidation webhook | + +\* Without OAuth credentials, JSON:API requests run as anonymous; protected content will return "Access denied". Use `--force-recreate` after changing env vars. + +### JSON:API Configuration +1. Navigate to `/admin/config/services/jsonapi/extras` +2. Configure resource types: + - Disable unwanted resources + - Rename fields for frontend convenience + - Configure includes and sparse fieldsets + +## JSON:API Endpoints + +### Examples +```bash +# Get all articles +curl https://cms.nasarek.dev/jsonapi/node/article + +# Get specific article +curl https://cms.nasarek.dev/jsonapi/node/article/{uuid} + +# Filter by status +curl https://cms.nasarek.dev/jsonapi/node/article?filter[status]=1 + +# Include related entities +curl https://cms.nasarek.dev/jsonapi/node/article?include=field_image,uid + +# Sparse fieldsets +curl https://cms.nasarek.dev/jsonapi/node/article?fields[node--article]=title,body,created + +# Sort and paginate +curl https://cms.nasarek.dev/jsonapi/node/article?sort=-created&page[limit]=10&page[offset]=0 +``` + +### With Authentication +```bash +# Get access token (client credentials - used by next-drupal) +curl -X POST https://cms.nasarek.dev/oauth/token \ + -d "grant_type=client_credentials" \ + -d "client_id=YOUR_CLIENT_UUID" \ + -d "client_secret=YOUR_SECRET" + +# Use token +curl https://cms.nasarek.dev/jsonapi/node/article \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +## Debugging + +### Enable Xdebug +Xdebug is pre-configured and enabled: +- Mode: debug +- Client host: host.docker.internal +- Client port: 9000 +- IDE key: VSCODE +- Log: `/var/log/xdebug.log` + +### View Logs +```bash +# PHP-FPM logs +docker logs drupal-fpm + +# NGINX logs +docker logs drupal-reverse-proxy + +# Xdebug log +docker exec drupal-fpm tail -f /var/log/xdebug.log +``` + +### Check PHP Configuration +```bash +# PHP version +docker exec drupal-fpm php -v + +# PHP modules +docker exec drupal-fpm php -m + +# PHP info +docker exec drupal-fpm php -i + +# OPcache status +docker exec drupal-fpm php -r "echo json_encode(opcache_get_status(), JSON_PRETTY_PRINT);" +``` + +## Troubleshooting + +### Module not found +```bash +# Check if module is installed +dcomposer show drupal/module_name + +# Install if missing +dcomposer require drupal/module_name +``` + +### Clear all caches +```bash +# Drupal cache +ddrush cr + +# Redis cache +docker exec drupal-redis redis-cli flushall + +# OPcache (restart PHP-FPM) +docker compose -f drupal/docker-compose.yml restart drupal-fpm +``` + +### Permissions issues +```bash +# Fix file permissions +docker exec drupal-fpm chown -R www-data:www-data /opt/drupal/web/sites/default/files + +# Settings.php permissions +docker exec drupal-fpm chmod 444 /opt/drupal/web/sites/default/settings.php +``` + +## File Structure +``` +drupal/ +├── docker-compose.yml # Service definitions +├── install-headless-modules.sh # Module installation script +├── drupal/ +│ ├── Dockerfile # PHP-FPM image +│ └── root/ # Drupal codebase (mounted volume) +│ ├── composer.json +│ ├── web/ # Document root +│ │ ├── core/ +│ │ ├── modules/ +│ │ │ ├── contrib/ +│ │ │ └── custom/ +│ │ ├── themes/ +│ │ └── sites/ +│ └── oauth/ +│ └── keys/ # OAuth RSA keys +└── nginx/ + ├── Dockerfile + └── nginx.conf.template # NGINX configuration +``` + +## Development Workflow + +1. Make changes to code in `drupal/root/` +2. Clear cache: `ddrush cr` (or `docker exec drupal-fpm bash -c "cd /opt/drupal && drush cr"`) +3. Export config: `ddrush cex -y` (or `docker exec drupal-fpm bash -c "cd /opt/drupal && drush cex -y"`) +4. Commit changes to git +5. Deploy: Pull changes and run `ddrush cim -y` on production + +## Production Checklist + +- [ ] Set `opcache.validate_timestamps=0` in production +- [ ] Configure Redis cache +- [ ] Generate OAuth keys +- [ ] Configure CORS restrictively +- [ ] Set up content revalidation webhooks +- [ ] Configure rate limiting +- [ ] Enable HTTPS only +- [ ] Set secure session cookies +- [ ] Configure backup strategy +- [ ] Monitor logs and performance +- [ ] Test JSON:API endpoints with different user roles + +## Resources + +- [Next.js for Drupal](https://next-drupal.org/) +- [JSON:API Documentation](https://www.drupal.org/docs/core-modules-and-themes/core-modules/jsonapi-module) +- [Simple OAuth](https://www.drupal.org/project/simple_oauth) +- [Drupal.org](https://www.drupal.org/) diff --git a/drupal/docker-compose.override.yml.disabled b/drupal/docker-compose.override.yml.disabled new file mode 100644 index 0000000..36c9604 --- /dev/null +++ b/drupal/docker-compose.override.yml.disabled @@ -0,0 +1,27 @@ +# Development overrides: bind mounts for Next.js hot reload. +# Loaded automatically by Docker Compose. Remove or rename to disable. +services: + nextjs: + build: + context: ./nextjs + dockerfile: Dockerfile + target: development + args: + NEXT_PUBLIC_DRUPAL_BASE_URL: https://cms.${DOMAIN} + volumes: + - ./nextjs:/app + - nextjs-node-modules:/app/node_modules + - nextjs-next-cache:/app/.next + ports: + - "9229:9229" + environment: + - NODE_ENV=production + - HOSTNAME=0.0.0.0 + - WATCHPACK_POLLING=false + - NODE_OPTIONS=--inspect=0.0.0.0:9229 + +volumes: + nextjs-node-modules: + name: drupal-nextjs-node-modules + nextjs-next-cache: + name: drupal-nextjs-next-cache diff --git a/drupal/docker-compose.yml b/drupal/docker-compose.yml index 08081e7..56f9a82 100644 --- a/drupal/docker-compose.yml +++ b/drupal/docker-compose.yml @@ -1,6 +1,7 @@ services: + # Drupal CMS NGINX reverse proxy (cms.nasarek.dev). nginx: - image: drupal-nginx + image: rnsrk/drupal-nginx build: context: ./nginx dockerfile: Dockerfile @@ -10,7 +11,7 @@ services: labels: - traefik.enable=true - traefik.docker.network=traefik - - traefik.http.routers.drupal-reverse-proxy.rule=Host(`${DOMAIN}`) + - traefik.http.routers.drupal-reverse-proxy.rule=Host(`cms.${DOMAIN}`) - traefik.http.routers.drupal-reverse-proxy.entrypoints=web,websecure - traefik.http.routers.drupal-reverse-proxy.middlewares=https-redirect - traefik.http.routers.drupal-reverse-proxy.tls=true @@ -22,13 +23,14 @@ services: - traefik - drupal + # Drupal PHP-FPM backend. drupal-fpm: - image: drupal-php8-4-fpm-bookworm + image: rnsrk/drupal-php8-4-fpm-bookworm build: context: ./drupal dockerfile: Dockerfile args: - DRUPAL_VERSION: ${DRUPAL_VERSION:-11.1.6} + DRUPAL_VERSION: ${DRUPAL_VERSION:-11.3.3} labels: - traefik.enable=false container_name: drupal-fpm @@ -40,12 +42,11 @@ services: - database - drupal + # Redis cache backend. redis: image: redis:7-alpine container_name: drupal-redis command: redis-server --loglevel warning - environment: - - OVERC volumes: - redis-data:/data networks: @@ -57,6 +58,37 @@ services: retries: 5 start_period: 10s + # Next.js frontend (nasarek.dev). + nextjs: + image: rnsrk/nextjs-frontend + build: + context: ./nextjs + dockerfile: Dockerfile + args: + NEXT_PUBLIC_DRUPAL_BASE_URL: https://cms.${DOMAIN} + DRUPAL_CLIENT_ID: ${DRUPAL_CLIENT_ID} + DRUPAL_CLIENT_SECRET: ${DRUPAL_CLIENT_SECRET} + DRUPAL_OAUTH_SCOPE: ${DRUPAL_OAUTH_SCOPE:-} + + container_name: nextjs-frontend + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.http.routers.nextjs-frontend.rule=Host(`${DOMAIN}`) + - traefik.http.routers.nextjs-frontend.entrypoints=web,websecure + - traefik.http.routers.nextjs-frontend.middlewares=https-redirect + - traefik.http.routers.nextjs-frontend.tls=true + - traefik.http.routers.nextjs-frontend.tls.certresolver=le + - traefik.http.services.nextjs-frontend.loadbalancer.server.port=3000 + env_file: + - ./nextjs/.env.local + networks: + - traefik + - drupal + depends_on: + - drupal-fpm + restart: unless-stopped + volumes: redis-data: name: drupal-redis-data diff --git a/drupal/drupal/Dockerfile b/drupal/drupal/Dockerfile index b11e33e..4c7a8c3 100644 --- a/drupal/drupal/Dockerfile +++ b/drupal/drupal/Dockerfile @@ -1,7 +1,8 @@ -ARG DRUPAL_VERSION +ARG DRUPAL_VERSION=11.3.3 FROM drupal:${DRUPAL_VERSION}-php8.4-fpm-bookworm +ARG NODE_ENV=production RUN apt-get update && apt-get install -y \ git \ vim \ @@ -14,13 +15,13 @@ RUN set -eux; \ docker-php-ext-install uploadprogress; \ rm -rf /usr/src/php/ext/uploadprogress; -# Install apcu +# Install apcu. RUN set -eux; \ - pecl install apcu; + pecl install apcu; \ + docker-php-ext-enable apcu; -# Add php configs +# Configure apcu (extension already loaded by docker-php-ext-enable). RUN { \ - echo 'extension=apcu.so'; \ echo "apc.enable_cli=1"; \ echo "apc.enable=1"; \ echo "apc.shm_size=32M"; \ @@ -30,3 +31,82 @@ RUN { \ RUN { \ echo 'output_buffering = on'; \ } >> /usr/local/etc/php/conf.d/zz-drupal-recommended.ini; + +# Enable Xdebug in development environment. +RUN if [ "$NODE_ENV" = "development" ]; then \ + set -eux; \ + pecl install xdebug; \ + docker-php-ext-enable xdebug; \ +fi; + +# Configure Xdebug in development environment. +# Configure xdebug (extension already loaded by docker-php-ext-enable). +RUN if [ "$NODE_ENV" = "development" ]; then \ + { \ + echo 'xdebug.mode=debug'; \ + echo 'xdebug.start_with_request=yes'; \ + echo 'xdebug.client_host=host.docker.internal'; \ + echo 'xdebug.client_port=9000'; \ + echo 'xdebug.idekey=VSCODE'; \ + echo 'xdebug.log=/var/log/xdebug.log'; \ + } >> /usr/local/etc/php/conf.d/zz-xdebug-custom.ini; \ +fi + +# Install and enable opcache. +RUN set -eux; \ + docker-php-ext-install opcache; \ + docker-php-ext-enable opcache + +# Configure opcache: dev (revalidate on) vs production (revalidate off). +RUN if [ "$NODE_ENV" = "development" ]; then \ + { \ + echo 'opcache.enable=1'; \ + echo 'opcache.enable_cli=1'; \ + echo 'opcache.memory_consumption=128'; \ + echo 'opcache.interned_strings_buffer=16'; \ + echo 'opcache.max_accelerated_files=10000'; \ + echo 'opcache.save_comments=1'; \ + echo 'opcache.validate_timestamps=1'; \ + echo 'opcache.revalidate_freq=0'; \ + echo 'opcache.fast_shutdown=1'; \ + } > /usr/local/etc/php/conf.d/zz-opcache-custom.ini; \ +else \ + { \ + echo 'opcache.enable=1'; \ + echo 'opcache.enable_cli=0'; \ + echo 'opcache.memory_consumption=256'; \ + echo 'opcache.interned_strings_buffer=32'; \ + echo 'opcache.max_accelerated_files=20000'; \ + echo 'opcache.save_comments=1'; \ + echo 'opcache.validate_timestamps=0'; \ + echo 'opcache.fast_shutdown=1'; \ + } > /usr/local/etc/php/conf.d/zz-opcache-custom.ini; \ +fi + +# Install Redis PHP extension. +RUN set -eux; \ + pecl install redis; \ + docker-php-ext-enable redis; + +# Install Node.js via NVM for frontend tooling. +ENV NVM_DIR=/root/.nvm +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash \ + && . "$NVM_DIR/nvm.sh" \ + && nvm install --lts \ + && nvm alias default 'lts/*' \ + && nvm use default \ + && node -v \ + && npm -v + +# Production PHP settings (disable display_errors for clean JSON:API responses). +RUN { \ + echo 'display_errors = Off'; \ + echo 'log_errors = On'; \ + echo 'error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT'; \ + } >> /usr/local/etc/php/conf.d/zz-production.ini + +# Note: Drupal modules should be installed via composer in the project directory. +# The /opt/drupal directory is mounted as a volume, so composer require. +# commands here would be lost on container start. +# Install modules by running: +# docker exec drupal-fpm bash -c "cd /opt/drupal && composer require drupal/module_name" diff --git a/drupal/install-headless-modules.sh b/drupal/install-headless-modules.sh new file mode 100755 index 0000000..1cb7480 --- /dev/null +++ b/drupal/install-headless-modules.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e + +echo "Installing Headless Drupal modules..." + +# Check if aliases are available. +if command -v dcomposer &> /dev/null; then + COMPOSER_CMD="dcomposer" + DRUSH_CMD="ddrush" +else + COMPOSER_CMD="docker exec drupal-fpm composer" + DRUSH_CMD="docker exec drupal-fpm drush" +fi + +# Core headless modules. +echo "→ Installing core headless modules..." +$COMPOSER_CMD require \ + drupal/next \ + drupal/jsonapi_extras \ + drupal/simple_oauth \ + drupal/pathauto \ + +# Additional JSON:API enhancements. +echo "→ Installing JSON:API enhancement modules..." +$COMPOSER_CMD require \ + drupal/subrequests \ + drupal/decoupled_router \ + drupal/consumers \ + drupal/jsonapi_menu_items \ + drupal/jsonapi_include \ + drupal/jsonapi_resources \ + drupal/jsonapi_menu_items + +# Content and media. +echo "→ Installing content and media modules..." +$COMPOSER_CMD require \ + drupal/metatag \ + drupal/redirect \ + drupal/field_group + +# Performance and caching. +echo "→ Installing performance modules..." +$COMPOSER_CMD require \ + drupal/redis + +# Development tools. +echo "→ Installing development tools..." +$COMPOSER_CMD require --dev \ + drupal/coder \ + drupal/devel \ + drupal/restui \ + drupal/admin_toolbar + +# "Advanced" modules. +echo "→ Installing advanced modules..." +$COMPOSER_CMD require \ + drupal/pathauto + +echo "✓ All modules installed successfully!" +echo "" +echo "Next steps:" +echo "1. Enable modules: $DRUSH_CMD en next jsonapi_extras simple_oauth pathauto -y" +echo "2. Generate OAuth keys: docker exec drupal-fpm bash -c 'cd /opt/drupal && mkdir -p oauth/keys && openssl genrsa -out oauth/keys/private.key 2048 && openssl rsa -in oauth/keys/private.key -pubout -out oauth/keys/public.key && chmod 600 oauth/keys/*.key'" +echo "3. Configure modules via Drupal admin UI" +echo "4. Export configuration: $DRUSH_CMD cex -y" diff --git a/drupal/nextjs/.dockerignore b/drupal/nextjs/.dockerignore new file mode 100644 index 0000000..69bb413 --- /dev/null +++ b/drupal/nextjs/.dockerignore @@ -0,0 +1,7 @@ +node_modules +.next +.git +*.md +.env*.local +Dockerfile +.dockerignore diff --git a/drupal/nextjs/.gitignore b/drupal/nextjs/.gitignore new file mode 100644 index 0000000..3c63d97 --- /dev/null +++ b/drupal/nextjs/.gitignore @@ -0,0 +1,25 @@ +# Dependencies +node_modules +.pnp +.pnp.js + +# Build +.next +out +build +dist + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Local env +.env*.local + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts diff --git a/drupal/nextjs/Dockerfile b/drupal/nextjs/Dockerfile new file mode 100644 index 0000000..b6a57af --- /dev/null +++ b/drupal/nextjs/Dockerfile @@ -0,0 +1,61 @@ +# Stage 0: Development (bind-mount source, run next dev). +FROM node:22-alpine AS development +RUN apk add --no-cache libc6-compat +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci || npm install +COPY . . +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +ENV NODE_ENV=development +# WATCHPACK_POLLING helps with bind mounts on some file systems. +ENV WATCHPACK_POLLING=true +ENTRYPOINT ["sh", "-c", "[ -d node_modules/.bin ] || npm install; exec npm run dev"] + +# Stage 1: Install dependencies. +FROM node:22-alpine AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci || npm install + +# Stage 2: Build the application. +FROM node:22-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build arguments for environment variables needed at build time. +ARG NEXT_PUBLIC_DRUPAL_BASE_URL +ARG DRUPAL_CLIENT_ID +ARG DRUPAL_CLIENT_SECRET +ARG DRUPAL_OAUTH_SCOPE +ENV NEXT_PUBLIC_DRUPAL_BASE_URL=${NEXT_PUBLIC_DRUPAL_BASE_URL} +ENV DRUPAL_CLIENT_ID=${DRUPAL_CLIENT_ID} +ENV DRUPAL_CLIENT_SECRET=${DRUPAL_CLIENT_SECRET} +ENV DRUPAL_OAUTH_SCOPE=${DRUPAL_OAUTH_SCOPE} + +RUN npm run build + +# Stage 3: Production runner. +FROM node:22-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Copy standalone output. +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +CMD ["node", "server.js"] diff --git a/drupal/nextjs/app/[...slug]/page.tsx b/drupal/nextjs/app/[...slug]/page.tsx new file mode 100644 index 0000000..8b2b267 --- /dev/null +++ b/drupal/nextjs/app/[...slug]/page.tsx @@ -0,0 +1,89 @@ +import { drupal } from "@/lib/drupal" +import type { DrupalNode } from "@/lib/types" +import { NodeArticle } from "@/components/node-article" +import { notFound } from "next/navigation" +import type { Metadata } from "next" + +const drupalBaseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL ?? "" + +interface NodePageProps { + params: Promise<{ + slug: string[] + }> +} + +// Render dynamically at runtime (not at build time). +export const dynamic = "force-dynamic" +export const revalidate = 60 + +export async function generateMetadata({ + params, +}: NodePageProps): Promise { + if (!drupalBaseUrl) return {} + + const { slug } = await params + const path = drupal.constructPathFromSegment(slug) + + try { + const translatedPath = await drupal.translatePath(path, { withAuth: true }) + + if (!translatedPath?.jsonapi?.resourceName || !translatedPath?.entity?.uuid) { + return {} + } + + const node = await drupal.getResource( + translatedPath.jsonapi.resourceName, + translatedPath.entity.uuid, + { + withAuth: true, + params: { + "fields[node--article]": "title", + "fields[node--page]": "title", + "fields[node--about]": "title", + }, + } + ) + + return { + title: node?.title, + } + } catch { + return {} + } +} + +export default async function NodePage({ params }: NodePageProps) { + if (!drupalBaseUrl) notFound() + + const { slug } = await params + const path = drupal.constructPathFromSegment(slug) + + try { + const translatedPath = await drupal.translatePath(path, { withAuth: true }) + + if (!translatedPath?.jsonapi?.resourceName || !translatedPath?.entity?.uuid) { + notFound() + } + + const type = translatedPath.jsonapi.resourceName + + const node = await drupal.getResource( + type, + translatedPath.entity.uuid, + { + withAuth: true, + params: { + include: "uid", + }, + } + ) + + if (!node || !node.status) { + notFound() + } + + return + } catch { + notFound() + } +} diff --git a/drupal/nextjs/app/api/disable-draft/route.ts b/drupal/nextjs/app/api/disable-draft/route.ts new file mode 100644 index 0000000..8190094 --- /dev/null +++ b/drupal/nextjs/app/api/disable-draft/route.ts @@ -0,0 +1,6 @@ +import { disableDraftMode } from "next-drupal/draft" +import type { NextRequest } from "next/server" + +export async function GET(request: NextRequest) { + return disableDraftMode() +} diff --git a/drupal/nextjs/app/api/draft/route.ts b/drupal/nextjs/app/api/draft/route.ts new file mode 100644 index 0000000..b8757e2 --- /dev/null +++ b/drupal/nextjs/app/api/draft/route.ts @@ -0,0 +1,7 @@ +import { drupal } from "@/lib/drupal" +import { enableDraftMode } from "next-drupal/draft" +import type { NextRequest } from "next/server" + +export async function GET(request: NextRequest): Promise { + return enableDraftMode(request, drupal) +} diff --git a/drupal/nextjs/app/api/preview/route.ts b/drupal/nextjs/app/api/preview/route.ts new file mode 100644 index 0000000..ff4e93f --- /dev/null +++ b/drupal/nextjs/app/api/preview/route.ts @@ -0,0 +1,9 @@ +import { enableDraftMode } from "next-drupal/draft" +import { drupal } from "@/lib/drupal" +import { NextRequest } from "next/server" + +export const dynamic = "force-dynamic" + +export async function GET(request: NextRequest) { + return enableDraftMode(request, drupal) +} diff --git a/drupal/nextjs/app/api/revalidate/route.ts b/drupal/nextjs/app/api/revalidate/route.ts new file mode 100644 index 0000000..b9357ce --- /dev/null +++ b/drupal/nextjs/app/api/revalidate/route.ts @@ -0,0 +1,35 @@ +import { revalidatePath } from "next/cache" +import { NextRequest } from "next/server" + +async function handler(request: NextRequest) { + const searchParams = request.nextUrl.searchParams + const secret = searchParams.get("secret") + const path = searchParams.get("path") + + // Validate the revalidation secret. + if (secret !== process.env.DRUPAL_REVALIDATE_SECRET) { + return new Response("Invalid secret.", { status: 401 }) + } + + if (!path) { + return new Response("Missing path.", { status: 400 }) + } + + try { + revalidatePath(path) + return new Response( + JSON.stringify({ revalidated: true, now: Date.now() }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ) + } catch (error) { + return new Response( + JSON.stringify({ message: "Error revalidating.", error }), + { status: 500 } + ) + } +} + +export { handler as GET, handler as POST } diff --git a/drupal/nextjs/app/favicon.ico b/drupal/nextjs/app/favicon.ico new file mode 100644 index 0000000..8300f98 Binary files /dev/null and b/drupal/nextjs/app/favicon.ico differ diff --git a/drupal/nextjs/app/globals.css b/drupal/nextjs/app/globals.css new file mode 100644 index 0000000..078aa74 --- /dev/null +++ b/drupal/nextjs/app/globals.css @@ -0,0 +1,307 @@ +@import "tailwindcss"; + +:root { + --accent: var(--color-emerald-600); + --accent-hex: #e11d48; + --fluid-hero: clamp(1.75rem, 4vw + 1rem, 3.75rem); + --fluid-hero-desc: clamp(1rem, 1.5vw + 0.75rem, 1.25rem); + --fluid-section-title: clamp(1.5rem, 3vw + 0.75rem, 1.875rem); + /* Fade-in on load: About starts when hero title animation ends (~2.1s), Services after About. */ + --fade-about-delay: 2.1s; + --fade-about-duration: 1.2s; + --fade-services-delay: 2.5s; + --fade-services-duration: 1.2s; +} + +/* Footer link icons: tint to emerald on link hover/focus (icons are img/SVG with fixed fill). */ +.group:hover .footer-icon-hover-emerald, +.group:focus-visible .footer-icon-hover-emerald { + filter: brightness(0) saturate(100%) invert(48%) sepia(79%) saturate(2476%) hue-rotate(130deg) brightness(95%) contrast(101%); +} + +@layer base { + a { + @apply transition-colors duration-200 ease-out; + } +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(16px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes marquee { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-50%); + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@keyframes float { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-4px); + } +} + +.animate-fade-in-up { + animation: fade-in-up 0.6s ease-out forwards; +} + +.animate-fade-in-on-load { + animation: fade-in var(--fade-about-duration) ease-in-out var(--fade-about-delay) both; +} + +.animate-fade-in-on-load-delayed { + animation: fade-in var(--fade-services-duration) ease-in-out var(--fade-services-delay) both; +} + +.animate-delay-100 { + animation-delay: 100ms; +} + +.animate-delay-200 { + animation-delay: 200ms; +} + +.animate-delay-300 { + animation-delay: 300ms; +} + +.animate-marquee { + animation: marquee 30s linear infinite; +} + +.animate-marquee-slow { + animation: marquee 60s linear infinite; +} + +.home-clients-band { + mask-image: linear-gradient( + to right, + transparent 0%, + black 8%, + black 92%, + transparent 100% + ); + -webkit-mask-image: linear-gradient( + to right, + transparent 0%, + black 8%, + black 92%, + transparent 100% + ); +} + +.animate-float { + animation: float 3s ease-in-out infinite; +} + +.animate-spin-slow { + animation: spin 8s linear infinite; +} + +.animate-spin-once { + animation: spin 0.5s ease-in-out 1 forwards; +} + +@keyframes coin-spin { + from { + transform: rotateY(0deg); + } + to { + transform: rotateY(720deg); + } +} + +.animate-coin-spin { + animation: coin-spin 0.6s ease-in-out 1 forwards; +} + +/* Hero title letter animations. */ +@keyframes letter-from-down { + from { + opacity: 0; + transform: translateY(100%); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes letter-from-up { + from { + opacity: 0; + transform: translateY(-100%); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes letter-b-exit { + 0% { + opacity: 0; + transform: translateY(-100%); + } + 20% { + opacity: 1; + transform: translateY(0); + } + 50% { + opacity: 1; + transform: translateY(0); + } + 51% { + opacity: 0; + transform: translateY(0); + } + 100% { + opacity: 0; + transform: translateY(0); + } +} + +@keyframes letter-o-in-out { + 0% { + opacity: 0; + transform: translateY(-100%); + } + 20% { + opacity: 1; + transform: translateY(0); + } + 70% { + opacity: 1; + transform: translateY(0); + } + 100% { + opacity: 0; + transform: translateY(-100%); + } +} + +@keyframes letter-e-fade-in { + from { + opacity: 0; + transform: translateY(100%); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* t: appears with o (Robot), stays, then slides right as r appears. */ +@keyframes letter-t-slide { + 0% { + opacity: 0; + transform: translateY(-100%) translateX(-0.7ch); + } + 20% { + opacity: 1; + transform: translateY(-0.45em) translateX(-0.7ch); + } + 55% { + opacity: 1; + transform: translateY(-0.45em) translateX(-0.7ch); + } + 100% { + opacity: 1; + transform: translateY(-0.45em) translateX(2px); + } +} + +.animate-letter-from-down { + animation: letter-from-down 0.5s ease-out both; +} + +.animate-letter-from-up { + animation: letter-from-up 0.5s ease-out both; +} + +.animate-letter-b-exit { + animation: letter-b-exit 0.9s ease-out both; +} + +.animate-letter-o-in-out { + animation: letter-o-in-out 1.5s ease-out both; +} + +.animate-letter-e-fade-in { + animation: letter-e-fade-in 0.5s ease-out both; +} + +.animate-letter-t-slide { + animation: letter-t-slide 0.9s ease-out both; +} + +.hero-letter-t { + overflow: visible; + vertical-align: baseline; +} + +.hero-letter-r { + position: relative; +} + +.hero-letter-r::before { + content: "B"; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + animation: letter-b-exit 0.6s ease-out both; + animation-delay: var(--b-delay, 320ms); +} + +.hero-letter-e { + position: relative; + padding-right: 1px; + padding-left: 1px; +} + +.hero-letter-e::before { + content: "o"; + position: absolute; + inset: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: center; + animation: letter-o-in-out 1.5s ease-out both; + animation-delay: var(--o-delay, 760ms); +} + +.layout-footer-section { + margin: 0 auto; +} diff --git a/drupal/nextjs/app/icon-robot-optimized.svg b/drupal/nextjs/app/icon-robot-optimized.svg new file mode 100644 index 0000000..ff101ca --- /dev/null +++ b/drupal/nextjs/app/icon-robot-optimized.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/drupal/nextjs/app/icon.svg b/drupal/nextjs/app/icon.svg new file mode 100644 index 0000000..665d52c --- /dev/null +++ b/drupal/nextjs/app/icon.svg @@ -0,0 +1,1517 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/drupal/nextjs/app/imprint/page.tsx b/drupal/nextjs/app/imprint/page.tsx new file mode 100644 index 0000000..404b536 --- /dev/null +++ b/drupal/nextjs/app/imprint/page.tsx @@ -0,0 +1,106 @@ +import type { Metadata } from "next" +import { ObfuscatedAddress } from "@/components/obfuscated-address" +import { ObfuscatedEmail } from "@/components/obfuscated-email" +import { drupal } from "@/lib/drupal" + +const drupalBaseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL ?? "" + + +export const dynamic = "force-dynamic" + +export const metadata: Metadata = { + title: "Imprint", + description: "Legal notice and imprint for nasarek.dev", +} + +const FALLBACK_TITLE = "Imprint" + +const BODY_STYLES = "[&_h2]:mb-4 [&_h2]:mt-8 [&_h2]:text-xl [&_h2]:font-semibold [&_h2]:text-slate-900 [&_p]:mb-4 [&_p]:text-slate-700 [&_a]:text-emerald-600 [&_a]:underline hover:[&_a]:text-emerald-500" + +/** + * Splits the CMS body HTML at {address} and {email} placeholders and renders + * the obfuscated components in their place so bots cannot harvest the data. + */ +function ImprintBody({ html }: { html: string }) { + const parts = html.split(/(

\{(?:address|email)\}<\/p>)/g) + + return ( +

+ {parts.map((part, i) => { + if (part === "

{address}

") return + if (part === "

{email}

") return

+ if (!part) return null + return
+ })} +
+ ) +} + +async function getImprintPageContent(): Promise<{ + title: string + body: string | null +}> { + if (!drupalBaseUrl) { + return { + title: FALLBACK_TITLE, + body: null, + } + } + + try { + const translatedPath = await drupal.translatePath("/imprint", { + withAuth: true, + next: { revalidate: 60 }, + }) + if (!translatedPath?.jsonapi?.resourceName || !translatedPath?.entity?.uuid) { + return { title: FALLBACK_TITLE, body: null } + } + + const resourceType = translatedPath.jsonapi.resourceName + const raw = await drupal.getResource( + resourceType, + translatedPath.entity.uuid, + { withAuth: true, next: { revalidate: 60 }, deserialize: false } + ) + const rawData = (raw as { data?: Record })?.data + if (!rawData) { + return { title: FALLBACK_TITLE, body: null } + } + + const title = (rawData.title as string) ?? FALLBACK_TITLE + const bodyObj = rawData.body + const bodyText = + typeof bodyObj === "string" + ? bodyObj + : (bodyObj as { processed?: string; value?: string })?.processed ?? + (bodyObj as { processed?: string; value?: string })?.value ?? + "" + + return { + title, + body: bodyText || null, + } + } catch (error) { + if ((error as Error).name !== "AbortError") { + console.warn("[Imprint] CMS unreachable:", (error as Error).message) + } + return { title: FALLBACK_TITLE, body: null } + } +} + +export default async function ImprintPage() { + const { title, body } = await getImprintPageContent() + return ( +
+
+

+ {title} +

+
+
+ {body && } +
+
+ ) +} diff --git a/drupal/nextjs/app/layout.tsx b/drupal/nextjs/app/layout.tsx new file mode 100644 index 0000000..78a3d7e --- /dev/null +++ b/drupal/nextjs/app/layout.tsx @@ -0,0 +1,253 @@ +import type { Metadata } from "next" +import Image from "next/image" +import { Source_Sans_3 } from "next/font/google" +import { + LayoutGrid, + FileText, + Database, + Scale, +} from "lucide-react" +import { MainNav } from "@/components/main-nav" +import { CookieBanner } from "@/components/cookie-banner" +import "./globals.css" + +const sourceSans3 = Source_Sans_3({ + subsets: ["latin"], + display: "swap", +}) + +export const metadata: Metadata = { + title: { + default: "nasarek.dev", + template: "%s | nasarek.dev", + }, + description: "Powered by Drupal and Next.js", +} + +export default async function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + + + Skip to main content + +
+ +
+
+
+ {children} +
+
+ + + + + ) +} diff --git a/drupal/nextjs/app/not-found.tsx b/drupal/nextjs/app/not-found.tsx new file mode 100644 index 0000000..a88092b --- /dev/null +++ b/drupal/nextjs/app/not-found.tsx @@ -0,0 +1,19 @@ +export default function NotFound() { + return ( +
+

404

+

+ Page Not Found +

+

+ The page you are looking for does not exist. +

+ + Go Home + +
+ ) +} diff --git a/drupal/nextjs/app/page.tsx b/drupal/nextjs/app/page.tsx new file mode 100644 index 0000000..1743ca5 --- /dev/null +++ b/drupal/nextjs/app/page.tsx @@ -0,0 +1,30 @@ +import { HomeHero } from "@/components/home-hero" + +// Force dynamic so HomeAbout fetches at request time (OAuth env vars available in container, not at build). +export const dynamic = "force-dynamic" +import { HomeAbout } from "@/components/home-about" +import { HomeServices } from "@/components/home-services" +import { HomeProjects } from "@/components/home-projects" +import { HomeClients } from "@/components/home-clients" +import { HomeJourneyBackground } from "@/components/home-journey-background" +import { ScrollRevealSection } from "@/components/scroll-reveal-section" + +export default function HomePage() { + return ( + + + + + +
+ +
+ + + + + + +
+ ) +} diff --git a/drupal/nextjs/app/resources/page.tsx b/drupal/nextjs/app/resources/page.tsx new file mode 100644 index 0000000..e0eff15 --- /dev/null +++ b/drupal/nextjs/app/resources/page.tsx @@ -0,0 +1,65 @@ +import type { Metadata } from "next" +import Link from "next/link" +import { FileText, Database, ArrowRight } from "lucide-react" + +export const metadata: Metadata = { + title: "Resources", + description: "Browse articles and models on nasarek.dev", +} + +export default function ResourcesPage() { + return ( +
+
+

+ Resources +

+

+ Explore my curated collection of articles and models. +

+
+ +
+ +
+ +
+

+ Articles +

+

+ Written content covering tutorials, guides, and insights. Articles + are published pieces with full text, images, and structured + formatting. +

+ + Browse articles + + + + + +
+ +
+

+ Datamodelling +

+

+ Structured data models and schemas from my projects. +

+ + Browse models + + + +
+
+ ) +} diff --git a/drupal/nextjs/components/animated-hero-title.tsx b/drupal/nextjs/components/animated-hero-title.tsx new file mode 100644 index 0000000..d993433 --- /dev/null +++ b/drupal/nextjs/components/animated-hero-title.tsx @@ -0,0 +1,78 @@ +"use client" + +const TEXT = "I'm Robert." + +type Direction = "up" | "down" | "r" | "e" | "t" + +const LETTER_ANIMS: Record = { + 0: "down", // I + 1: "up", // ' + 2: "down", // m + 3: "down", // space - treat as down for delay + 4: "r", // R (special) + 5: "down", // o + 6: "up", // b + 7: "e", // e (special: o first, then e) + 8: "down", // r + 9: "t", // t (special: appears with o, then slides right) + 10: "down", // . +} + +const DELAYS_MS = [0, 80, 240, 320, 400, 560, 680, 760, 1400, 760, 1560] + +export function AnimatedHeroTitle() { + return ( +

+ {TEXT.split("").map((char, i) => { + if (char === " ") { + return + } + const dir = LETTER_ANIMS[i] ?? "down" + const delay = DELAYS_MS[i] ?? i * 80 + + const animClass = + dir === "r" + ? "hero-letter-r animate-letter-from-up" + : dir === "e" + ? "hero-letter-e" + : dir === "t" + ? "hero-letter-t animate-letter-t-slide" + : dir === "up" + ? "animate-letter-from-up" + : "animate-letter-from-down" + const animDelay = + dir === "r" ? delay + 320 : dir === "e" ? delay + 1050 : delay + const style = + dir === "r" + ? { "--b-delay": `${delay}ms`, animationDelay: `${animDelay}ms` } as React.CSSProperties + : dir === "e" + ? { "--o-delay": `${delay}ms` } as React.CSSProperties + : { animationDelay: `${animDelay}ms` } + + return ( + + {dir === "e" ? ( + + e + + ) : dir === "r" ? ( + "R" + ) : dir === "t" ? ( + "t" + ) : ( + char + )} + + ) + })} +

+ ) +} diff --git a/drupal/nextjs/components/avatar-image.tsx b/drupal/nextjs/components/avatar-image.tsx new file mode 100644 index 0000000..ab5cd77 --- /dev/null +++ b/drupal/nextjs/components/avatar-image.tsx @@ -0,0 +1,58 @@ +"use client" + +import Image from "next/image" +import { useCallback, useState } from "react" + +const IMAGE_POOL = [ + "/assets/images/autumn.png", + "/assets/images/kuss.png", + "/assets/images/chaos.png", + "/assets/images/conference.png", + "/assets/images/explaining.png", + "/assets/images/family.png", + "/assets/images/pres_1.png", + "/assets/images/robot.png", +] as const + +function pickRandom(exclude?: string): string { + const available = exclude + ? IMAGE_POOL.filter((p) => p !== exclude) + : [...IMAGE_POOL] + return available[Math.floor(Math.random() * available.length)] +} + +export function AvatarImage({ alt }: { alt: string }) { + const [currentSrc, setCurrentSrc] = useState(() => pickRandom()) + const [isSpinning, setIsSpinning] = useState(false) + + const handleMouseEnter = useCallback(() => { + setIsSpinning(true) + setCurrentSrc((prev) => pickRandom(prev)) + }, []) + + const handleAnimationEnd = useCallback(() => { + setIsSpinning(false) + }, []) + + return ( +
+
+ {alt} +
+
+ ) +} diff --git a/drupal/nextjs/components/cookie-banner.tsx b/drupal/nextjs/components/cookie-banner.tsx new file mode 100644 index 0000000..a7b6b2d --- /dev/null +++ b/drupal/nextjs/components/cookie-banner.tsx @@ -0,0 +1,62 @@ +"use client" + +import { useState, useEffect } from "react" +import Link from "next/link" + +const CONSENT_COOKIE = "cookie-consent" +const CONSENT_MAX_AGE = 365 * 24 * 60 * 60 // 1 year in seconds + +function setConsentCookie() { + document.cookie = `${CONSENT_COOKIE}=accepted; path=/; max-age=${CONSENT_MAX_AGE}; SameSite=Lax` +} + +function hasConsent(): boolean { + if (typeof document === "undefined") return false + return document.cookie.includes(`${CONSENT_COOKIE}=accepted`) +} + +export function CookieBanner() { + const [isVisible, setIsVisible] = useState(false) + + useEffect(() => { + if (!hasConsent()) { + setIsVisible(true) + } + }, []) + + const handleAccept = () => { + setConsentCookie() + setIsVisible(false) + } + + if (!isVisible) return null + + return ( +
+
+

+ This site uses a single cookie to store your consent preference. No + tracking or analytics cookies are used.{" "} + + Learn more + +

+ +
+
+ ) +} diff --git a/drupal/nextjs/components/debug-trigger.tsx b/drupal/nextjs/components/debug-trigger.tsx new file mode 100644 index 0000000..095a782 --- /dev/null +++ b/drupal/nextjs/components/debug-trigger.tsx @@ -0,0 +1,16 @@ +"use client" + +import { useEffect } from "react" + +/** + * Triggers the browser debugger when the component mounts. + * Only runs in development. Remove when done debugging. + */ +export function DebugTrigger() { + useEffect(() => { + if (process.env.NODE_ENV === "development") { + debugger + } + }, []) + return null +} diff --git a/drupal/nextjs/components/home-about.tsx b/drupal/nextjs/components/home-about.tsx new file mode 100644 index 0000000..c60ac7c --- /dev/null +++ b/drupal/nextjs/components/home-about.tsx @@ -0,0 +1,127 @@ +import { drupal } from "@/lib/drupal" +import type { DrupalAboutNode } from "@/lib/types" +import { AvatarImage } from "./avatar-image" +import { MailToLink } from "./mail-to-link" + +const drupalBaseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL ?? "" + +const FALLBACK_TITLE = "Robert Nasarek" +function FallbackBody() { + return ( + <> +

+ Data Engineer & Developer +

+

+ I’m a freelance backend and data engineer specialising in data modelling, ETL pipelines, and data-centric application architecture. I design and implement scalable APIs and backend systems using Python and modern web frameworks like Next.js, Drupal, and Django to build robust data workflows for analytics and machine learning use cases. +

+

+ My focus is on semantic and structured data systems that turn heterogeneous sources into reliable, queryable, and reusable knowledge. I deliver production-ready solutions, including containerised deployments and reproducible data pipelines, with an emphasis on correctness, performance, and maintainability. +

+ + ) +} + +async function getAboutPageContent(): Promise<{ + title: string + body: string | null + email: string | null +}> { + if (!drupalBaseUrl) { + return { + title: FALLBACK_TITLE, + body: null, + email: null, + } + } + + try { + const translatedPath = await drupal.translatePath("/about/robert-nasarek", { + withAuth: true, + next: { revalidate: 60 }, + }) + if ( + !translatedPath?.jsonapi?.resourceName || + !translatedPath?.entity?.uuid || + translatedPath.jsonapi.resourceName !== "node--about" + ) { + return { title: FALLBACK_TITLE, body: null, email: null } + } + + const node = await drupal.getResource( + "node--about", + translatedPath.entity.uuid, + { withAuth: true, next: { revalidate: 60 } } + ) + + if (!node) { + return { title: FALLBACK_TITLE, body: null, email: null } + } + + const bodyObj = node.body + const bodyText = + typeof bodyObj === "string" + ? bodyObj + : bodyObj?.processed ?? bodyObj?.value ?? "" + + return { + title: node.title ?? FALLBACK_TITLE, + body: bodyText || null, + email: node.field_email ?? null, + } + } catch (error) { + if ((error as Error).name !== "AbortError") { + console.warn("[HomeAbout] CMS unreachable:", (error as Error).message) + } + return { title: FALLBACK_TITLE, body: null, email: null } + } +} + +export async function HomeAbout() { + const { title, body, email } = await getAboutPageContent() + return ( +
+
+

+ About me +

+

+ Data engineer and developer with a focus on linked open data, ontology engineering, and full-stack development. +

+
+
+
+ +
+

+ {title} +

+
+

+ Data Engineer & Developer +

+

+ Hi I’m a freelance backend and data engineer specialising in data modelling, ETL pipelines, and data-centric application architecture. I design and implement scalable APIs and backend systems using Python and modern web frameworks like Next.js, Drupal, and Django to build robust data workflows for analytics and machine learning use cases. +

+

+ My focus is on semantic and structured data systems that turn heterogeneous sources into reliable, queryable, and reusable knowledge. I deliver production-ready solutions, including containerised deployments and reproducible data pipelines, with an emphasis on correctness, performance, and maintainability. +

+
+

+ +

+
+
+
+
+ ) +} diff --git a/drupal/nextjs/components/home-clients.tsx b/drupal/nextjs/components/home-clients.tsx new file mode 100644 index 0000000..680f776 --- /dev/null +++ b/drupal/nextjs/components/home-clients.tsx @@ -0,0 +1,223 @@ +"use client" + +import Image from "next/image" +import { Building2 } from "lucide-react" +import { useState, useRef, useEffect } from "react" + +function ClientIcon({ icon }: { icon?: string }) { + const [hasError, setHasError] = useState(false) + + if (!icon || hasError) { + return + } + + return ( + setHasError(true)} + /> + ) +} + +const clients = [ + { + name: "Max Planck Institute for Social Anthropology", + location: "Halle/Saale", + href: "https://www.eth.mpg.de/", + icon: "/assets/icons/eth-mpg.png", + }, + { + name: "German National Academy of Sciences Leopoldina", + location: "Halle/Saale", + href: "https://www.leopoldina.org/", + icon: "/assets/icons/leopoldina.png", + }, + { + name: "German National Museum", + location: "Nuremberg", + href: "https://www.gnm.de/", + icon: "/assets/icons/gnm.png", + }, + { + name: "Central Institute for Art History", + location: "Munich", + href: "https://www.zikg.eu/", + icon: "/assets/icons/zikg.png", + }, + { + name: "German Fairy Tale and Weser Legends Museum", + location: "Bad Oeynhausen", + href: "https://www.badoeynhausen.de/freizeit-kultur-sport/kultur/staedtische-museen/deutsches-maerchen-und-wesersagenmuseum", + icon: "/assets/icons/badoeynhausen.png", + }, + { + name: "Roli-Bar", + location: "roli-bar.de", + href: "https://roli-bar.de/", + icon: "/assets/icons/roli-bar.png", + }, + { + name: "Re-Cycle Halle", + location: "Halle/Saale", + href: "https://re-cycle-halle.de/", + icon: "/assets/icons/re-cycle-halle.png", + }, + { + name: "bold + bündig", + location: "Leipzig", + href: "https://boldundbuendig.de/", + icon: "/assets/icons/boldundbuendig.png", + }, +] + +function ClientsBandContent() { + return ( + <> + {clients.map((client) => { + const content = ( + <> + + {client.name} + ({client.location}) + + ) + const className = + "home-clients-band-item mx-4 flex shrink-0 items-center gap-2 rounded-xl border border-slate-200 bg-white px-6 py-4 text-base text-slate-700 shadow-sm" + return client.href ? ( + + {content} + + ) : ( + + {content} + + ) + })} + + ) +} + +const SCROLL_SPEED = 40 + +export function HomeClients() { + const bandRef = useRef(null) + const trackRef = useRef(null) + const posRef = useRef(0) + const isHoveredRef = useRef(false) + const isDraggingRef = useRef(false) + const dragStartXRef = useRef(0) + const dragStartPosRef = useRef(0) + const lastTimeRef = useRef(null) + const rafRef = useRef(null) + const [isDragging, setIsDragging] = useState(false) + + useEffect(() => { + const track = trackRef.current + if (!track) return + + const step = (time: number) => { + const halfWidth = track.scrollWidth / 2 + if (halfWidth > 0 && lastTimeRef.current !== null && !isHoveredRef.current && !isDraggingRef.current) { + const dt = time - lastTimeRef.current + posRef.current += SCROLL_SPEED * dt / 1000 + if (posRef.current >= halfWidth) posRef.current -= halfWidth + } + lastTimeRef.current = time + track.style.transform = `translateX(${-posRef.current}px)` + rafRef.current = requestAnimationFrame(step) + } + + rafRef.current = requestAnimationFrame(step) + return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current) } + }, []) + + const handleMouseEnter = () => { isHoveredRef.current = true } + const handleMouseLeave = () => { + isHoveredRef.current = false + isDraggingRef.current = false + setIsDragging(false) + } + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault() + isDraggingRef.current = true + dragStartXRef.current = e.clientX + dragStartPosRef.current = posRef.current + setIsDragging(true) + } + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDraggingRef.current) return + const track = trackRef.current + if (!track) return + const halfWidth = track.scrollWidth / 2 + const dx = dragStartXRef.current - e.clientX + posRef.current = ((dragStartPosRef.current + dx) % halfWidth + halfWidth) % halfWidth + } + const handleMouseUp = () => { + isDraggingRef.current = false + setIsDragging(false) + } + + const handleTouchStart = (e: React.TouchEvent) => { + isDraggingRef.current = true + dragStartXRef.current = e.touches[0].clientX + dragStartPosRef.current = posRef.current + } + const handleTouchMove = (e: React.TouchEvent) => { + if (!isDraggingRef.current) return + const track = trackRef.current + if (!track) return + const halfWidth = track.scrollWidth / 2 + const dx = dragStartXRef.current - e.touches[0].clientX + posRef.current = ((dragStartPosRef.current + dx) % halfWidth + halfWidth) % halfWidth + } + const handleTouchEnd = () => { isDraggingRef.current = false } + + return ( +
+
+

+ Employers & Customers +

+

+ Research institutions and organisations I have worked with. +

+
+
+
+ + +
+
+
+ ) +} diff --git a/drupal/nextjs/components/home-cta.tsx b/drupal/nextjs/components/home-cta.tsx new file mode 100644 index 0000000..68a82fe --- /dev/null +++ b/drupal/nextjs/components/home-cta.tsx @@ -0,0 +1,24 @@ +import Link from "next/link" +import { ArrowRight } from "lucide-react" + +export function HomeCta() { + return ( +
+
+

+ Hi There! +

+

+ Browse articles and data models for your next project. +

+ + View all resources + + +
+
+ ) +} diff --git a/drupal/nextjs/components/home-features.tsx b/drupal/nextjs/components/home-features.tsx new file mode 100644 index 0000000..b782326 --- /dev/null +++ b/drupal/nextjs/components/home-features.tsx @@ -0,0 +1,63 @@ +import Link from "next/link" +import { FileText, Database, ArrowRight } from "lucide-react" + +const features = [ + { + title: "Articles", + description: + "Tutorials, guides, and insights on data engineering and software development.", + href: "/resources/articles", + icon: FileText, + color: "emerald", + }, + { + title: "Datamodelling", + description: + "Structured data models and schemas from real-world projects.", + href: "/resources/datamodelling", + icon: Database, + color: "fuchsia", + }, +] + +export function HomeFeatures() { + return ( +
+
+

+ Resources +

+

+ Use one or all. Curated content for data engineers and developers. +

+
+
+ {features.map((feature, index) => ( + +
+ +
+

+ {feature.title} +

+

{feature.description}

+ + Learn more + + + + ))} +
+
+ ) +} diff --git a/drupal/nextjs/components/home-hero.tsx b/drupal/nextjs/components/home-hero.tsx new file mode 100644 index 0000000..f013806 --- /dev/null +++ b/drupal/nextjs/components/home-hero.tsx @@ -0,0 +1,11 @@ +import { AnimatedHeroTitle } from "./animated-hero-title" + +export function HomeHero() { + return ( +
+
+ +
+
+ ) +} diff --git a/drupal/nextjs/components/home-journey-background.tsx b/drupal/nextjs/components/home-journey-background.tsx new file mode 100644 index 0000000..dfbb3ed --- /dev/null +++ b/drupal/nextjs/components/home-journey-background.tsx @@ -0,0 +1,196 @@ +"use client" + +import React, { useEffect, useState } from "react" +import { + Lightbulb, + PenTool, + Code2, + Rocket, + Wrench, + MessageSquare, + LifeBuoy, +} from "lucide-react" + +const STATION_COLORS: Record< + string, + { border: string; bg: string; text: string; label: string } +> = { + idea: { border: "border-amber-400", bg: "bg-amber-50", text: "text-amber-600", label: "text-amber-700" }, + concept: { border: "border-emerald-400", bg: "bg-emerald-50", text: "text-emerald-600", label: "text-emerald-700" }, + consulting: { border: "border-sky-400", bg: "bg-sky-50", text: "text-sky-600", label: "text-sky-700" }, + development: { border: "border-fuchsia-400", bg: "bg-fuchsia-100", text: "text-fuchsia-600", label: "text-fuchsia-700" }, + deployment: { border: "border-sky-400", bg: "bg-sky-50", text: "text-sky-600", label: "text-sky-700" }, + maintenance: { border: "border-slate-400", bg: "bg-slate-100", text: "text-slate-600", label: "text-slate-700" }, + support: { border: "border-rose-400", bg: "bg-rose-50", text: "text-rose-600", label: "text-rose-700" }, +} + +const STATIONS = [ + { id: "idea", icon: Lightbulb, label: "Idea", lineThreshold: 0 }, + { id: "concept", icon: PenTool, label: "Concept", lineThreshold: 15 }, + { id: "deployment", icon: Rocket, label: "Deployment", lineThreshold: 45 }, + { id: "maintenance", icon: Wrench, label: "Maintenance", lineThreshold: 80 }, +] as const + +const RIGHT_STATIONS = [ + { id: "consulting", icon: MessageSquare, label: "Consulting", lineThreshold: 0 }, + { id: "development", icon: Code2, label: "Development", lineThreshold: 25 }, + { id: "support", icon: LifeBuoy, label: "Support", lineThreshold: 60 }, +] as const + +export function HomeJourneyBackground({ children }: { children: React.ReactNode }) { + const childArray = React.Children.toArray(children) + const heroContent = childArray[0] + const mainContent = childArray.slice(1) + const [visibleStations, setVisibleStations] = useState>(new Set()) + const [visibleRightStations, setVisibleRightStations] = useState>( + new Set() + ) + useEffect(() => { + const handleScroll = () => { + const y = window.scrollY + + const docHeight = document.documentElement.scrollHeight - window.innerHeight + const scrollProgress = docHeight > 0 ? Math.min(y / docHeight, 1) : 0 + const ideaThreshold = 0.02 + const linePercent = + scrollProgress >= ideaThreshold + ? Math.min( + 100, + ((scrollProgress - ideaThreshold) / (1 - ideaThreshold)) * 100 + ) + : 0 + + const newVisible = new Set() + if (scrollProgress >= ideaThreshold) { + newVisible.add("idea") + } + STATIONS.forEach((station) => { + if (station.id !== "idea" && linePercent >= station.lineThreshold) { + newVisible.add(station.id) + } + }) + setVisibleStations(newVisible) + + const newVisibleRight = new Set() + if (scrollProgress >= ideaThreshold) { + newVisibleRight.add("consulting") + } + RIGHT_STATIONS.forEach((station) => { + if ( + station.id !== "consulting" && + linePercent >= station.lineThreshold + ) { + newVisibleRight.add(station.id) + } + }) + setVisibleRightStations(newVisibleRight) + } + + handleScroll() + window.addEventListener("scroll", handleScroll, { passive: true }) + return () => window.removeEventListener("scroll", handleScroll) + }, []) + + return ( +
+
{heroContent}
+
+ +
+ {mainContent} +
+ +
+
+ ) +} diff --git a/drupal/nextjs/components/home-marquee.tsx b/drupal/nextjs/components/home-marquee.tsx new file mode 100644 index 0000000..c0f1c7c --- /dev/null +++ b/drupal/nextjs/components/home-marquee.tsx @@ -0,0 +1,36 @@ +"use client" + +const testimonials = [ + "Clean, fast, and well documented.", + "Best developer experience I've had in years.", + "Went from zero to production in minutes.", + "The documentation is a joy to read.", + "Scales effortlessly with my needs.", + "Exactly what I needed for my project.", +] + +function MarqueeContent() { + return ( + <> + {testimonials.map((testimonial, index) => ( + + {testimonial} + + ))} + + ) +} + +export function HomeMarquee() { + return ( +
+
+ + +
+
+ ) +} diff --git a/drupal/nextjs/components/home-projects.tsx b/drupal/nextjs/components/home-projects.tsx new file mode 100644 index 0000000..3c8fe77 --- /dev/null +++ b/drupal/nextjs/components/home-projects.tsx @@ -0,0 +1,88 @@ +import Link from "next/link" +import Image from "next/image" +import { ArrowUpRight } from "lucide-react" +import { ScrollRevealCard } from "@/components/scroll-reveal-card" + +const projects = [ + { + title: "Böhler re:search", + description: + "Digital edition of the Munich art dealer Julius Böhler's object card system, photo folders and customer index (1903–1948). Research data on traded artworks, transactions and actors.", + href: "https://boehler.zikg.eu/", + icon: "/assets/icons/boehler-research.png", + }, + { + title: "Objektsprache und Ästhetik", + description: + "Shell collections at Leopoldina, Goldfuß-Museum Bonn, and Central Institute for Natural Collections MLU. Historical object references and synonym networks for conchylia.", + href: "https://konchylien.leopoldina.org/sammlungen", + icon: "/assets/logos/lzfw_logo.png", + }, + { + title: "SCS Manager", + description: + "Semantic Co-Working Space for academic university collections. Model, transform, analyse and publish data with JupyterLab, OpenRefine, WissKI and more.", + href: "https://manager.scs.sammlungen.io/", + icon: "/assets/icons/scs-manager.png", + }, + { + title: "WissKI", + description: + "Semantic data management system for GLAM institutions. Virtual research environment extending Drupal with CIDOC CRM, Pathbuilder and linked open data.", + href: "https://wiss-ki.eu/", + icon: "/assets/icons/wisski.svg", + }, +] + +export function HomeProjects() { + return ( +
+
+

+ Projects +

+

+ Data- and information-focused websites and applications. +

+
+
+ {projects.map((project) => ( + + +

+ + {project.title} +

+

+ {project.description} +

+ + Visit project + + + +
+ ))} +
+
+ ) +} diff --git a/drupal/nextjs/components/home-services.tsx b/drupal/nextjs/components/home-services.tsx new file mode 100644 index 0000000..5b103d2 --- /dev/null +++ b/drupal/nextjs/components/home-services.tsx @@ -0,0 +1,146 @@ +import { Book, Brain, Calendar, Cpu, Code2, Database, Rocket, Unplug, Wrench } from "lucide-react" +import type { LucideIcon } from "lucide-react" +import { drupal } from "@/lib/drupal" +import type { DrupalServiceNode } from "@/lib/types" +import { ScrollRevealCard } from "@/components/scroll-reveal-card" + +const drupalBaseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL ?? "" + +const ICON_MAP: Record = { + coordination: Calendar, + data_processing: Cpu, + deployment: Rocket, + development: Code2, + documentation: Book, + interface_api: Unplug, + interface_and_api: Unplug, + maintainance: Wrench, + maintenance: Wrench, + modelling: Database, + ai: Brain, +} + +function toIconKey(type: string): string { + return type.toLowerCase().replace(/\s+/g, "_").replace(/-/g, "_") +} + +function getIcon(serviceType: string | undefined): LucideIcon { + if (!serviceType) return Database + const key = toIconKey(serviceType) + return ICON_MAP[key] ?? Database +} + +function stripHtml(html: string | undefined): string { + if (!html) return "" + return html.replace(/<[^>]*>/g, "").trim() +} + +async function getServices(): Promise< + { label: string; body: string; icon: LucideIcon }[] +> { + if (!drupalBaseUrl) return [] + + try { + let raw: { data?: DrupalServiceNode[] } | null = null + try { + raw = await drupal.getResourceCollection<{ + data: DrupalServiceNode[] + }>("node--service", { + params: { + "filter[status]": "1", + sort: "created", + }, + deserialize: false, + next: { revalidate: 60 }, + }) + } catch (firstError) { + const msg = (firstError as Error).message ?? "" + if (msg.includes("Unauthorized")) { + await new Promise((r) => setTimeout(r, 1000)) + raw = await drupal.getResourceCollection<{ + data: DrupalServiceNode[] + }>("node--service", { + params: { + "filter[status]": "1", + sort: "created", + }, + deserialize: false, + next: { revalidate: 60 }, + }) + } else { + throw firstError + } + } + + const nodes = raw?.data ?? [] + if (!nodes.length) return [] + + return nodes.map((node) => { + const bodyObj = node.body + const bodyText = + typeof bodyObj === "string" + ? stripHtml(bodyObj) + : stripHtml(bodyObj?.value ?? bodyObj?.processed) + + return { + body: bodyText, + icon: getIcon(node.field__service__type), + label: node.title ?? "", + } + }) + } catch (error) { + if ((error as Error).name !== "AbortError") { + console.warn( + "[HomeServices] CMS unreachable:", + (error as Error).message + ) + } + return [] + } +} + +export async function HomeServices() { + const services = await getServices() + + return ( +
+
+

+ Services +

+

+ Data engineering, development, deployment, and ongoing support for + your projects. +

+
+
+ {services.map(({ label, body, icon: Icon }) => ( + +
+ +
+ + {label} + + {body && ( + + {body} + + )} +
+
+
+ ))} +
+
+ ) +} diff --git a/drupal/nextjs/components/imprint-body.tsx b/drupal/nextjs/components/imprint-body.tsx new file mode 100644 index 0000000..5cabff9 --- /dev/null +++ b/drupal/nextjs/components/imprint-body.tsx @@ -0,0 +1,57 @@ +"use client" + +import { ObfuscatedEmail } from "@/components/obfuscated-email" +import { ObfuscatedAddress } from "@/components/obfuscated-address" + +interface ImprintBodyProps { + html: string +} + +/** + * Renders imprint HTML with {email} and {address} placeholders replaced by + * ObfuscatedEmail and ObfuscatedAddress components. + */ +export function ImprintBody({ html }: ImprintBodyProps) { + const placeholderRegex = /\{(email|address)\}/g + const parts: (string | React.ReactNode)[] = [] + let lastIndex = 0 + let match + let key = 0 + + while ((match = placeholderRegex.exec(html)) !== null) { + const before = html.slice(lastIndex, match.index) + if (before) { + parts.push( + + ) + } + if (match[1] === "email") { + parts.push( + + ) + } else { + parts.push() + } + lastIndex = match.index + match[0].length + } + + const after = html.slice(lastIndex) + if (after) { + parts.push( + + ) + } + + return <>{parts} +} diff --git a/drupal/nextjs/components/mail-to-link.tsx b/drupal/nextjs/components/mail-to-link.tsx new file mode 100644 index 0000000..7972acc --- /dev/null +++ b/drupal/nextjs/components/mail-to-link.tsx @@ -0,0 +1,47 @@ +"use client" + +import { useEffect, useState } from "react" +import { Mail } from "lucide-react" + +interface MailToLinkProps { + /** When provided (e.g. from Drupal field_email), use this email. Otherwise use fallback. */ + email?: string | null +} + +/** + * Mailto link with icon; email is set on client to reduce harvestability when not passed as prop. + */ +export function MailToLink({ email }: MailToLinkProps) { + const [href, setHref] = useState(email ? `mailto:${email}` : null) + + useEffect(() => { + if (email) { + setHref(`mailto:${email}`) + return + } + const localPart = "robert" + const domain = "nasarek" + const tld = "dev" + setHref(`mailto:${localPart}@${domain}.${tld}`) + }, [email]) + + if (!href) { + return ( + + + Write me + + ) + } + + return ( + + + Write me + + ) +} diff --git a/drupal/nextjs/components/main-nav-client.tsx b/drupal/nextjs/components/main-nav-client.tsx new file mode 100644 index 0000000..99833b9 --- /dev/null +++ b/drupal/nextjs/components/main-nav-client.tsx @@ -0,0 +1,156 @@ +"use client" + +import type { DrupalMenuItem } from "next-drupal" +import { Home, FolderOpen, ChevronDown } from "lucide-react" +import { useState, useRef, useEffect } from "react" + +const drupalBaseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL ?? "" + +function getHref(url: string): string { + if (drupalBaseUrl && url.startsWith(drupalBaseUrl)) { + return url.slice(drupalBaseUrl.length) || "/" + } + return url +} + +function NavLink({ + item, + isDropdown = false, + onNavigate, +}: { + item: DrupalMenuItem + isDropdown?: boolean + onNavigate?: () => void +}) { + const children = item.items?.filter((child) => child.enabled !== false) ?? [] + const hasChildren = children.length > 0 + const linkClass = isDropdown + ? "block rounded-sm px-4 py-2 text-emerald-500 outline-none transition-colors duration-200 ease-out hover:underline focus-visible:ring-2 focus-visible:ring-emerald-400 focus-visible:ring-inset" + : "flex items-center gap-1.5 rounded-sm text-emerald-500 outline-none transition-colors duration-200 ease-out hover:text-emerald-400 hover:underline focus-visible:ring-2 focus-visible:ring-emerald-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-800" + + if (hasChildren) { + return ( +
+ + + {item.title} + + +
+
+ {children.map((child) => ( + + {child.title} + + ))} +
+
+
+ ) + } + + const href = getHref(item.url) + const isHome = href === "/" + return ( + + {isHome && } + {item.title} + + ) +} + +interface MainNavClientProps { + menuItems: DrupalMenuItem[] +} + +export function MainNavClient({ menuItems }: MainNavClientProps) { + const [isOpen, setIsOpen] = useState(false) + const menuRef = useRef(null) + + const enabledItems = menuItems.filter((item) => item.enabled !== false) + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + if (isOpen) { + document.addEventListener("click", handleClickOutside) + } + return () => document.removeEventListener("click", handleClickOutside) + }, [isOpen]) + + const closeMenu = () => setIsOpen(false) + + return ( + + ) +} diff --git a/drupal/nextjs/components/main-nav.tsx b/drupal/nextjs/components/main-nav.tsx new file mode 100644 index 0000000..5100a61 --- /dev/null +++ b/drupal/nextjs/components/main-nav.tsx @@ -0,0 +1,76 @@ +import type { DrupalMenuItem } from "next-drupal" +import { MainNavClient } from "./main-nav-client" + +const drupalBaseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL ?? "" + +interface RawMenuItem { + id: string + parent: string + title: string + url: string + enabled?: boolean + weight?: string | number +} + +function buildMenuTree( + items: RawMenuItem[], + parentId: string +): DrupalMenuItem[] { + return items + .filter((item) => (item.parent || "") === parentId && item.enabled !== false) + .sort((a, b) => Number(a.weight ?? 0) - Number(b.weight ?? 0)) + .map((item) => { + const children = buildMenuTree(items, item.id) + return { + ...item, + items: children.length ? children : undefined, + } as DrupalMenuItem + }) +} + +const FALLBACK_MENU: DrupalMenuItem[] = [ + { + id: "home", + title: "Home", + url: "/", + enabled: true, + items: undefined, + } as DrupalMenuItem, +] + +async function getMainMenu(): Promise { + if (!drupalBaseUrl) return FALLBACK_MENU + + try { + const url = `${drupalBaseUrl.replace(/\/$/, "")}/jsonapi/menu_items/main` + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 5000) + const res = await fetch(url, { + headers: { Accept: "application/vnd.api+json" }, + next: { revalidate: 60 }, + signal: controller.signal, + }) + clearTimeout(timeoutId) + if (!res.ok) { + if (res.status !== 404) { + console.warn(`[MainNav] Menu fetch returned ${res.status}, using fallback nav.`) + } + return FALLBACK_MENU + } + + const json = await res.json() + const items: RawMenuItem[] = json.data ?? [] + + return buildMenuTree(items, "") + } catch (error) { + if ((error as Error).name !== "AbortError") { + console.warn("[MainNav] CMS unreachable, using fallback nav:", (error as Error).message) + } + return FALLBACK_MENU + } +} + +export async function MainNav() { + const menuItems = await getMainMenu() + return +} diff --git a/drupal/nextjs/components/node-article-teaser.tsx b/drupal/nextjs/components/node-article-teaser.tsx new file mode 100644 index 0000000..e5628e6 --- /dev/null +++ b/drupal/nextjs/components/node-article-teaser.tsx @@ -0,0 +1,34 @@ +import type { DrupalNode } from "@/lib/types" + +interface NodeArticleTeaserProps { + node: DrupalNode +} + +export function NodeArticleTeaser({ node }: NodeArticleTeaserProps) { + const href = node.path?.alias || `/node/${node.id}` + + return ( +
+

+ + {node.title} + +

+
+ {node.uid?.display_name && ( + By {node.uid.display_name} + )} + +
+ {node.body?.summary && ( +

{node.body.summary}

+ )} +
+ ) +} diff --git a/drupal/nextjs/components/node-article.tsx b/drupal/nextjs/components/node-article.tsx new file mode 100644 index 0000000..a33566a --- /dev/null +++ b/drupal/nextjs/components/node-article.tsx @@ -0,0 +1,35 @@ +import type { DrupalNode } from "@/lib/types" + +interface NodeArticleProps { + node: DrupalNode +} + +export function NodeArticle({ node }: NodeArticleProps) { + return ( +
+

+ {node.title} +

+
+ {node.uid?.display_name && ( + By {node.uid.display_name} + )} + +
+ {(node.body?.processed ?? node.body?.value) && ( +
+ )} +
+ ) +} diff --git a/drupal/nextjs/components/node-page.tsx b/drupal/nextjs/components/node-page.tsx new file mode 100644 index 0000000..ea30624 --- /dev/null +++ b/drupal/nextjs/components/node-page.tsx @@ -0,0 +1,21 @@ +import type { DrupalNode } from "@/lib/types" + +interface NodePageProps { + node: DrupalNode +} + +export function NodePage({ node }: NodePageProps) { + return ( +
+

+ {node.title} +

+ {node.body?.processed && ( +
+ )} +
+ ) +} diff --git a/drupal/nextjs/components/obfuscated-address.tsx b/drupal/nextjs/components/obfuscated-address.tsx new file mode 100644 index 0000000..dd88f60 --- /dev/null +++ b/drupal/nextjs/components/obfuscated-address.tsx @@ -0,0 +1,49 @@ +"use client" + +import { useEffect, useState } from "react" + +/** + * Renders a mailto link only after client mount so the email is not in the + * server-rendered HTML, reducing harvestability by bots that scan static HTML. + * Parts are hardcoded so they live in the JS bundle, not in page HTML. + */ +type AddressData = { + fullname: string + street: string + city: string + country: string +} + +export function ObfuscatedAddress({ className }: { className?: string }) { + const [address, setAddress] = useState(null) + + useEffect(() => { + setAddress({ + fullname: "Robert Nasarek", + street: "Kleine Ulrichstraße 1", + city: "Halle (Saale)", + country: "Germany", + }) + }, []) + + if (!address) { + return ( + + + + + ) + } + + return ( +

+ {address.fullname} +
+ {address.street} +
+ {address.city} +
+ {address.country} +

+ ) +} diff --git a/drupal/nextjs/components/obfuscated-email.tsx b/drupal/nextjs/components/obfuscated-email.tsx new file mode 100644 index 0000000..e2d8978 --- /dev/null +++ b/drupal/nextjs/components/obfuscated-email.tsx @@ -0,0 +1,34 @@ +"use client" + +import { useEffect, useState } from "react" + +/** + * Renders a mailto link only after client mount so the email is not in the + * server-rendered HTML, reducing harvestability by bots that scan static HTML. + * Parts are hardcoded so they live in the JS bundle, not in page HTML. + */ +export function ObfuscatedEmail({ className }: { className?: string }) { + const [email, setEmail] = useState(null) + + useEffect(() => { + const localPart = "robert" + const domain = "nasarek" + const tld = "dev" + setEmail(`${localPart}@${domain}.${tld}`) + }, []) + + if (!email) { + return ( + + + + + ) + } + + return ( + + {email} + + ) +} diff --git a/drupal/nextjs/components/scroll-reveal-card.tsx b/drupal/nextjs/components/scroll-reveal-card.tsx new file mode 100644 index 0000000..6ca44bc --- /dev/null +++ b/drupal/nextjs/components/scroll-reveal-card.tsx @@ -0,0 +1,38 @@ +"use client" + +import { useEffect, useRef, useState, type ReactNode } from "react" + +interface ScrollRevealCardProps { + children: ReactNode +} + +export function ScrollRevealCard({ children }: ScrollRevealCardProps) { + const ref = useRef(null) + const [isVisible, setIsVisible] = useState(false) + + useEffect(() => { + const el = ref.current + if (!el) return + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) setIsVisible(true) + }, + { threshold: 0.35, rootMargin: "0px 0px -120px 0px" } + ) + + observer.observe(el) + return () => observer.disconnect() + }, []) + + return ( +
+ {children} +
+ ) +} diff --git a/drupal/nextjs/components/scroll-reveal-section.tsx b/drupal/nextjs/components/scroll-reveal-section.tsx new file mode 100644 index 0000000..fd0af6c --- /dev/null +++ b/drupal/nextjs/components/scroll-reveal-section.tsx @@ -0,0 +1,56 @@ +"use client" + +import { useEffect, useRef, useState, type ReactNode } from "react" + +interface ScrollRevealSectionProps { + children: ReactNode + /** When true, section starts visible (no opacity-20 flash). Use for above-the-fold hero. */ + initialVisible?: boolean + /** Delay in ms before the reveal animation starts after the section enters view. */ + revealDelayMs?: number +} + +export function ScrollRevealSection({ + children, + initialVisible = false, + revealDelayMs = 0, +}: ScrollRevealSectionProps) { + const ref = useRef(null) + const [isVisible, setIsVisible] = useState(initialVisible) + + useEffect(() => { + const el = ref.current + if (!el) return + + let timeoutId: ReturnType | null = null + + const observer = new IntersectionObserver( + ([entry]) => { + if (!entry.isIntersecting) return + if (revealDelayMs <= 0) { + setIsVisible(true) + return + } + timeoutId = setTimeout(() => setIsVisible(true), revealDelayMs) + }, + { threshold: 0.1, rootMargin: "0px 0px -80px 0px" } + ) + + observer.observe(el) + return () => { + if (timeoutId) clearTimeout(timeoutId) + observer.disconnect() + } + }, [revealDelayMs]) + + return ( +
+ {children} +
+ ) +} diff --git a/drupal/nextjs/lib/config.ts b/drupal/nextjs/lib/config.ts new file mode 100644 index 0000000..e69de29 diff --git a/drupal/nextjs/lib/drupal.ts b/drupal/nextjs/lib/drupal.ts new file mode 100644 index 0000000..df0bb70 --- /dev/null +++ b/drupal/nextjs/lib/drupal.ts @@ -0,0 +1,20 @@ +import { NextDrupal } from "next-drupal" + +const baseUrl = process.env.NEXT_PUBLIC_DRUPAL_BASE_URL! + +const auth = + process.env.DRUPAL_CLIENT_ID && process.env.DRUPAL_CLIENT_SECRET + ? { + clientId: process.env.DRUPAL_CLIENT_ID, + clientSecret: process.env.DRUPAL_CLIENT_SECRET, + ...(process.env.DRUPAL_OAUTH_SCOPE && { + scope: process.env.DRUPAL_OAUTH_SCOPE, + }), + } + : undefined + +export const drupal = new NextDrupal(baseUrl, { + auth, + withAuth: !!auth, + debug: process.env.NODE_ENV === "development", +}) diff --git a/drupal/nextjs/lib/types.ts b/drupal/nextjs/lib/types.ts new file mode 100644 index 0000000..a232d45 --- /dev/null +++ b/drupal/nextjs/lib/types.ts @@ -0,0 +1,71 @@ +import type { JsonApiResource } from "next-drupal" + +// Drupal JSON:API resource types. + +export interface DrupalNode extends JsonApiResource { + title: string + status: boolean + created: string + changed: string + path: { + alias: string + pid: number + langcode: string + } + body?: { + value: string + format: string + processed: string + summary: string + } + field_image?: DrupalMedia + uid?: { + id: string + display_name: string + } + metatag?: DrupalMetatag[] +} + +export interface DrupalMedia extends JsonApiResource { + name: string + field_media_image?: DrupalFile +} + +export interface DrupalFile extends JsonApiResource { + uri: { + value: string + url: string + } + resourceIdObjMeta?: { + alt: string + title: string + width: number + height: number + } +} + +export interface DrupalMetatag { + tag: string + attributes: Record +} + +export interface DrupalMenuLinkContent { + id: string + title: string + url: string + parent: string + weight: number + expanded: boolean + enabled: boolean + items?: DrupalMenuLinkContent[] +} + +export interface DrupalServiceNode extends DrupalNode { + /** Service type from Drupal (modelling, development, deployment, etc.). JSON:API exposes as field__service__type. */ + field__service__type?: string +} + +export interface DrupalAboutNode extends DrupalNode { + /** JSON:API resource type: node--about. */ + field_email?: string +} diff --git a/drupal/nextjs/next.config.ts b/drupal/nextjs/next.config.ts new file mode 100644 index 0000000..dd1ca4c --- /dev/null +++ b/drupal/nextjs/next.config.ts @@ -0,0 +1,20 @@ +import type { NextConfig } from "next" + +const nextConfig: NextConfig = { + images: { + qualities: [75, 95], + remotePatterns: [ + { + protocol: "https", + hostname: process.env.NEXT_IMAGE_DOMAIN || "cms.nasarek.dev", + }, + ], + }, + // Enable standalone output for Docker. + output: "standalone", + + // DevIndicators + devIndicators: false, +} + +export default nextConfig diff --git a/drupal/nextjs/package-lock.json b/drupal/nextjs/package-lock.json new file mode 100644 index 0000000..d88ee31 --- /dev/null +++ b/drupal/nextjs/package-lock.json @@ -0,0 +1,1969 @@ +{ + "name": "nasarek-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nasarek-frontend", + "version": "1.0.0", + "dependencies": { + "lucide-react": "^0.574.0", + "next": "^15.1", + "next-drupal": "^2.0.0", + "react": "^19.0", + "react-dom": "^19.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.0", + "@types/node": "^22.0", + "@types/react": "^19.0", + "@types/react-dom": "^19.0", + "tailwindcss": "^4.0", + "typescript": "^5.7" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz", + "integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz", + "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz", + "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz", + "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz", + "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz", + "integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz", + "integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz", + "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz", + "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jsona": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/jsona/-/jsona-1.12.1.tgz", + "integrity": "sha512-44WL4ZdsKx//mCDPUFQtbK7mnVdHXcVzbBy7Pzy0LAgXyfpN5+q8Hum7cLUX4wTnRsClHb4eId1hePZYchwczg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.4.1" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lucide-react": { + "version": "0.574.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.574.0.tgz", + "integrity": "sha512-dJ8xb5juiZVIbdSn3HTyHsjjIwUwZ4FNwV0RtYDScOyySOeie1oXZTymST6YPJ4Qwt3Po8g4quhYl4OxtACiuQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", + "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.12", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.12", + "@next/swc-darwin-x64": "15.5.12", + "@next/swc-linux-arm64-gnu": "15.5.12", + "@next/swc-linux-arm64-musl": "15.5.12", + "@next/swc-linux-x64-gnu": "15.5.12", + "@next/swc-linux-x64-musl": "15.5.12", + "@next/swc-win32-arm64-msvc": "15.5.12", + "@next/swc-win32-x64-msvc": "15.5.12", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-drupal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/next-drupal/-/next-drupal-2.0.0.tgz", + "integrity": "sha512-cgkfdSe2iepaiJkG4586SyCrUoXNsCfj9r1CcChidDZN7iDPLSKIIq8i63Yf0lPzLz+amwVW9lhWwrErOy8OIA==", + "license": "MIT", + "dependencies": { + "jsona": "^1.12.1", + "next": "^14.2.21 || ^15.1.2", + "node-cache": "^5.1.2", + "qs": "^6.13.1", + "react": "^18.2 || ^19.0", + "react-dom": "^18.2 || ^19.0" + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/drupal/nextjs/package.json b/drupal/nextjs/package.json new file mode 100644 index 0000000..1110f70 --- /dev/null +++ b/drupal/nextjs/package.json @@ -0,0 +1,29 @@ +{ + "name": "nasarek-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "dev:debug": "NODE_OPTIONS='--inspect' next dev", + "build": "next build", + "start": "next start -p 3000", + "lint": "next lint", + "docker:build": "docker build -t rnsrk/nextjs-frontend --build-arg NEXT_PUBLIC_DRUPAL_BASE_URL=https://cms.nasarek.dev .", + "docker:up": "cd .. && docker compose up -d nextjs --build" + }, + "dependencies": { + "lucide-react": "^0.574.0", + "next": "^15.1", + "next-drupal": "^2.0.0", + "react": "^19.0", + "react-dom": "^19.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4.0", + "@types/node": "^22.0", + "@types/react": "^19.0", + "@types/react-dom": "^19.0", + "tailwindcss": "^4.0", + "typescript": "^5.7" + } +} diff --git a/drupal/nextjs/postcss.config.mjs b/drupal/nextjs/postcss.config.mjs new file mode 100644 index 0000000..f6c75ff --- /dev/null +++ b/drupal/nextjs/postcss.config.mjs @@ -0,0 +1,8 @@ +/** @type {import('postcss-load-config').Config} */ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +} + +export default config diff --git a/drupal/nextjs/public/assets/icons/badoeynhausen.png b/drupal/nextjs/public/assets/icons/badoeynhausen.png new file mode 100644 index 0000000..2ec8cd3 Binary files /dev/null and b/drupal/nextjs/public/assets/icons/badoeynhausen.png differ diff --git a/drupal/nextjs/public/assets/icons/boehler-research.png b/drupal/nextjs/public/assets/icons/boehler-research.png new file mode 100644 index 0000000..ac17085 Binary files /dev/null and b/drupal/nextjs/public/assets/icons/boehler-research.png differ diff --git a/drupal/nextjs/public/assets/icons/boldundbuendig.png b/drupal/nextjs/public/assets/icons/boldundbuendig.png new file mode 100644 index 0000000..57c8306 Binary files /dev/null and b/drupal/nextjs/public/assets/icons/boldundbuendig.png differ diff --git a/drupal/nextjs/public/assets/icons/drupal.svg b/drupal/nextjs/public/assets/icons/drupal.svg new file mode 100644 index 0000000..e932dbb --- /dev/null +++ b/drupal/nextjs/public/assets/icons/drupal.svg @@ -0,0 +1 @@ +Drupal diff --git a/drupal/nextjs/public/assets/icons/eth-mpg.png b/drupal/nextjs/public/assets/icons/eth-mpg.png new file mode 100644 index 0000000..3c6f7ff Binary files /dev/null and b/drupal/nextjs/public/assets/icons/eth-mpg.png differ diff --git a/drupal/nextjs/public/assets/icons/github.svg b/drupal/nextjs/public/assets/icons/github.svg new file mode 100644 index 0000000..62c6e57 --- /dev/null +++ b/drupal/nextjs/public/assets/icons/github.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/drupal/nextjs/public/assets/icons/gnm.png b/drupal/nextjs/public/assets/icons/gnm.png new file mode 100644 index 0000000..709da7c Binary files /dev/null and b/drupal/nextjs/public/assets/icons/gnm.png differ diff --git a/drupal/nextjs/public/assets/icons/leopoldina.png b/drupal/nextjs/public/assets/icons/leopoldina.png new file mode 100644 index 0000000..d88d1de Binary files /dev/null and b/drupal/nextjs/public/assets/icons/leopoldina.png differ diff --git a/drupal/nextjs/public/assets/icons/mastodon.svg b/drupal/nextjs/public/assets/icons/mastodon.svg new file mode 100644 index 0000000..750bba2 --- /dev/null +++ b/drupal/nextjs/public/assets/icons/mastodon.svg @@ -0,0 +1,3 @@ + + + diff --git a/drupal/nextjs/public/assets/icons/nextjs.svg b/drupal/nextjs/public/assets/icons/nextjs.svg new file mode 100644 index 0000000..58a452e --- /dev/null +++ b/drupal/nextjs/public/assets/icons/nextjs.svg @@ -0,0 +1,4 @@ + + + + diff --git a/drupal/nextjs/public/assets/icons/objektsprache.png b/drupal/nextjs/public/assets/icons/objektsprache.png new file mode 100644 index 0000000..1e660b7 Binary files /dev/null and b/drupal/nextjs/public/assets/icons/objektsprache.png differ diff --git a/drupal/nextjs/public/assets/icons/re-cycle-halle.png b/drupal/nextjs/public/assets/icons/re-cycle-halle.png new file mode 100644 index 0000000..d1a4609 Binary files /dev/null and b/drupal/nextjs/public/assets/icons/re-cycle-halle.png differ diff --git a/drupal/nextjs/public/assets/icons/roli-bar.png b/drupal/nextjs/public/assets/icons/roli-bar.png new file mode 100644 index 0000000..0a692a9 Binary files /dev/null and b/drupal/nextjs/public/assets/icons/roli-bar.png differ diff --git a/drupal/nextjs/public/assets/icons/scs-manager.png b/drupal/nextjs/public/assets/icons/scs-manager.png new file mode 100644 index 0000000..1eef8c1 Binary files /dev/null and b/drupal/nextjs/public/assets/icons/scs-manager.png differ diff --git a/drupal/nextjs/public/assets/icons/spotify.svg b/drupal/nextjs/public/assets/icons/spotify.svg new file mode 100644 index 0000000..9ca691a --- /dev/null +++ b/drupal/nextjs/public/assets/icons/spotify.svg @@ -0,0 +1,4 @@ + + + + diff --git a/drupal/nextjs/public/assets/icons/wisski.svg b/drupal/nextjs/public/assets/icons/wisski.svg new file mode 100644 index 0000000..3f952fc --- /dev/null +++ b/drupal/nextjs/public/assets/icons/wisski.svg @@ -0,0 +1,687 @@ + + diff --git a/drupal/nextjs/public/assets/icons/youtube.svg b/drupal/nextjs/public/assets/icons/youtube.svg new file mode 100644 index 0000000..f4fcd50 --- /dev/null +++ b/drupal/nextjs/public/assets/icons/youtube.svg @@ -0,0 +1,20 @@ + + + + +Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + diff --git a/drupal/nextjs/public/assets/icons/zenodo.svg b/drupal/nextjs/public/assets/icons/zenodo.svg new file mode 100644 index 0000000..fc8cc76 --- /dev/null +++ b/drupal/nextjs/public/assets/icons/zenodo.svg @@ -0,0 +1,3 @@ + + + diff --git a/drupal/nextjs/public/assets/icons/zikg.png b/drupal/nextjs/public/assets/icons/zikg.png new file mode 100644 index 0000000..cfc97e9 Binary files /dev/null and b/drupal/nextjs/public/assets/icons/zikg.png differ diff --git a/drupal/nextjs/public/assets/images/autumn.png b/drupal/nextjs/public/assets/images/autumn.png new file mode 100644 index 0000000..79eaa46 Binary files /dev/null and b/drupal/nextjs/public/assets/images/autumn.png differ diff --git a/drupal/nextjs/public/assets/images/chaos.png b/drupal/nextjs/public/assets/images/chaos.png new file mode 100644 index 0000000..0481889 Binary files /dev/null and b/drupal/nextjs/public/assets/images/chaos.png differ diff --git a/drupal/nextjs/public/assets/images/conference.png b/drupal/nextjs/public/assets/images/conference.png new file mode 100644 index 0000000..f51bcd2 Binary files /dev/null and b/drupal/nextjs/public/assets/images/conference.png differ diff --git a/drupal/nextjs/public/assets/images/explaining.png b/drupal/nextjs/public/assets/images/explaining.png new file mode 100644 index 0000000..66737df Binary files /dev/null and b/drupal/nextjs/public/assets/images/explaining.png differ diff --git a/drupal/nextjs/public/assets/images/family.png b/drupal/nextjs/public/assets/images/family.png new file mode 100644 index 0000000..56fd980 Binary files /dev/null and b/drupal/nextjs/public/assets/images/family.png differ diff --git a/drupal/nextjs/public/assets/images/kuss.png b/drupal/nextjs/public/assets/images/kuss.png new file mode 100644 index 0000000..b758911 Binary files /dev/null and b/drupal/nextjs/public/assets/images/kuss.png differ diff --git a/drupal/nextjs/public/assets/images/pres_1.png b/drupal/nextjs/public/assets/images/pres_1.png new file mode 100644 index 0000000..824fc67 Binary files /dev/null and b/drupal/nextjs/public/assets/images/pres_1.png differ diff --git a/drupal/nextjs/public/assets/images/robot.png b/drupal/nextjs/public/assets/images/robot.png new file mode 100644 index 0000000..f07c203 Binary files /dev/null and b/drupal/nextjs/public/assets/images/robot.png differ diff --git a/drupal/nextjs/public/assets/logos/lzfw_logo.png b/drupal/nextjs/public/assets/logos/lzfw_logo.png new file mode 100644 index 0000000..56b2bd0 Binary files /dev/null and b/drupal/nextjs/public/assets/logos/lzfw_logo.png differ diff --git a/drupal/nextjs/tsconfig.json b/drupal/nextjs/tsconfig.json new file mode 100644 index 0000000..d8b9323 --- /dev/null +++ b/drupal/nextjs/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/drupal/nginx/Dockerfile b/drupal/nginx/Dockerfile index 0ffadfd..efbcffd 100644 --- a/drupal/nginx/Dockerfile +++ b/drupal/nginx/Dockerfile @@ -3,6 +3,6 @@ FROM nginx:latest COPY ./nginx.conf.template /etc/nginx/nginx.conf.template ARG DOMAIN -RUN sed 's|${DOMAIN}|'"$DOMAIN"'|g' /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf +RUN envsubst '${DOMAIN}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf ENTRYPOINT ["nginx", "-g", "daemon off;"] diff --git a/drupal/nginx/nginx.conf.template b/drupal/nginx/nginx.conf.template index 5aee951..7d56f22 100644 --- a/drupal/nginx/nginx.conf.template +++ b/drupal/nginx/nginx.conf.template @@ -18,11 +18,21 @@ http { keepalive_timeout 65; gzip on; + # Increase client body size for file uploads. + client_max_body_size 64M; + server { listen 80; - server_name ${DOMAIN}; + server_name cms.${DOMAIN}; root /var/www/html; + # JSON:API endpoint caching headers. + location /jsonapi { + try_files $uri /index.php$is_args$args; + add_header Cache-Control "public, max-age=60"; + add_header X-Content-Type-Options nosniff; + } + location / { try_files $uri /index.php$is_args$args; } @@ -33,9 +43,10 @@ http { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $document_root; + fastcgi_read_timeout 120; } - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { try_files $uri @rewrite; expires max; log_not_found off; @@ -45,19 +56,19 @@ http { rewrite ^ /index.php; } - # Don't allow direct access to PHP files in the vendor directory + # Don't allow direct access to PHP files in the vendor directory. location ~ /vendor/.*\.php$ { deny all; return 404; } - # Protect files and directories from prying eyes + # Protect files and directories from prying eyes. location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ { deny all; return 404; } - # Protect .git directory + # Protect .git directory. location ~ /\.git { deny all; return 404; diff --git a/forgejo-runner/docker-compose.yml b/forgejo-runner/docker-compose.yml new file mode 100644 index 0000000..320038a --- /dev/null +++ b/forgejo-runner/docker-compose.yml @@ -0,0 +1,68 @@ +services: + # Docker-in-Docker backend so CI jobs run in isolated containers, + # not against the host Docker daemon. + docker-in-docker: + image: docker:dind + container_name: forgejo-runner-dind + privileged: true + environment: + DOCKER_TLS_CERTDIR: /certs + volumes: + - runner-certs:/certs/client + - forgejo-runner-dind:/var/lib/docker + networks: + forgejo-runner: + # DinD's TLS cert is issued for the SAN "docker"; alias so the + # runner can verify the cert when connecting over TLS. + aliases: + - docker + restart: unless-stopped + + forgejo-runner: + image: code.forgejo.org/forgejo/runner:${RUNNER_VERSION:-6} + container_name: forgejo-runner + environment: + DOCKER_HOST: tcp://docker:2376 + DOCKER_CERT_PATH: /certs/client + DOCKER_TLS_VERIFY: "1" + FORGEJO_INSTANCE_URL: ${FORGEJO_INSTANCE_URL} + FORGEJO_RUNNER_TOKEN: ${FORGEJO_RUNNER_TOKEN} + FORGEJO_RUNNER_NAME: ${FORGEJO_RUNNER_NAME:-forgejo-runner} + entrypoint: ["/bin/sh", "-c"] + command: + - | + set -e + if [ ! -f /data/.runner ]; then + echo "Registering runner with ${FORGEJO_INSTANCE_URL} ..." + forgejo-runner register --no-interactive \ + --instance "${FORGEJO_INSTANCE_URL}" \ + --token "${FORGEJO_RUNNER_TOKEN}" \ + --name "${FORGEJO_RUNNER_NAME}" \ + --labels "docker:docker://node:20-bookworm,ubuntu-latest:docker://node:20-bookworm" + fi + exec forgejo-runner daemon + working_dir: /data + volumes: + - forgejo-runner-data:/data + - runner-certs:/certs/client:ro + networks: + - forgejo-runner + - traefik + depends_on: + - docker-in-docker + restart: unless-stopped + +volumes: + forgejo-runner-data: + name: forgejo-runner-data + forgejo-runner-dind: + name: forgejo-runner-dind + runner-certs: + name: forgejo-runner-certs + +networks: + forgejo-runner: + name: forgejo-runner + traefik: + name: traefik + external: true diff --git a/forgejo/docker-compose.yml b/forgejo/docker-compose.yml new file mode 100644 index 0000000..fd9a3ef --- /dev/null +++ b/forgejo/docker-compose.yml @@ -0,0 +1,58 @@ +services: + # Forgejo git forge (git.nasarek.dev). Replaces GitLab. + forgejo: + image: codeberg.org/forgejo/forgejo:${FORGEJO_VERSION:-11} + container_name: forgejo + environment: + USER_UID: 1000 + USER_GID: 1000 + FORGEJO__database__DB_TYPE: postgres + FORGEJO__database__HOST: postgres:5432 + FORGEJO__database__NAME: ${FORGEJO_DB_NAME} + FORGEJO__database__USER: ${FORGEJO_DB_USER} + FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD} + FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN} + FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN}/ + FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN} + FORGEJO__server__HTTP_PORT: "3000" + # Advertised in clone URLs (Traefik forgejo-ssh entrypoint, formerly GitLab's port). + FORGEJO__server__SSH_PORT: "2424" + FORGEJO__server__SSH_LISTEN_PORT: "22" + FORGEJO__actions__ENABLED: "true" + # Skip the web installer; auto-migrate against Postgres on boot. + FORGEJO__security__INSTALL_LOCK: "true" + FORGEJO__service__DISABLE_REGISTRATION: "true" + labels: + - traefik.enable=true + - traefik.docker.network=traefik + # HTTP + - traefik.http.routers.forgejo.rule=Host(`${FORGEJO_DOMAIN}`) + - traefik.http.routers.forgejo.entrypoints=web,websecure + - traefik.http.routers.forgejo.middlewares=https-redirect + - traefik.http.routers.forgejo.tls=true + - traefik.http.routers.forgejo.tls.certresolver=le + - traefik.http.services.forgejo.loadbalancer.server.port=3000 + # SSH over dedicated Traefik TCP entrypoint (port 2424) + - "traefik.tcp.routers.forgejo-ssh.rule=HostSNI(`*`)" + - "traefik.tcp.routers.forgejo-ssh.entrypoints=forgejo-ssh" + - "traefik.tcp.services.forgejo-ssh.loadbalancer.server.port=22" + volumes: + - forgejo-data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + networks: + - traefik + - database + restart: unless-stopped + +volumes: + forgejo-data: + name: forgejo-data + +networks: + traefik: + name: traefik + external: true + database: + name: database + external: true diff --git a/forgejo/migrate-from-gitlab.sh b/forgejo/migrate-from-gitlab.sh new file mode 100755 index 0000000..2f44944 --- /dev/null +++ b/forgejo/migrate-from-gitlab.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# +# Bulk-migrate every GitLab project into Forgejo with full data +# (code, issues, merge requests, labels, milestones, releases, wiki). +# +# Runs both forges in parallel — GitLab is only read, never modified. +# +# Usage: +# export GITLAB_URL=https://gitlab.nasarek.dev +# export GITLAB_TOKEN=glpat-xxxxxxxx # scopes: read_api, read_repository +# export FORGEJO_URL=https://git.nasarek.dev +# export FORGEJO_TOKEN=xxxxxxxx # Forgejo app token, repo + org scope +# export FORGEJO_OWNER=root # target user/org that will own the repos +# ./migrate-from-gitlab.sh [--dry-run] +# +set -euo pipefail + +DRY_RUN=false +[ "${1:-}" = "--dry-run" ] && DRY_RUN=true + +: "${GITLAB_URL:?set GITLAB_URL}" +: "${GITLAB_TOKEN:?set GITLAB_TOKEN}" +: "${FORGEJO_URL:?set FORGEJO_URL}" +: "${FORGEJO_TOKEN:?set FORGEJO_TOKEN}" +: "${FORGEJO_OWNER:?set FORGEJO_OWNER (target user/org)}" + +command -v jq >/dev/null || { echo "jq is required"; exit 1; } + +# Resolve the Forgejo owner's numeric uid (repos/migrate needs repo_owner name + uid). +OWNER_UID=$(curl -fsS -H "Authorization: token ${FORGEJO_TOKEN}" \ + "${FORGEJO_URL}/api/v1/users/${FORGEJO_OWNER}" | jq -r '.id') +echo "Target owner: ${FORGEJO_OWNER} (uid=${OWNER_UID})" + +migrate_one() { + local clone_url="$1" name="$2" private="$3" desc="$4" + echo "==> ${name} (private=${private})" + if $DRY_RUN; then return 0; fi + + local payload + payload=$(jq -n \ + --arg addr "$clone_url" --arg token "$GITLAB_TOKEN" \ + --arg name "$name" --arg owner "$FORGEJO_OWNER" \ + --argjson uid "$OWNER_UID" --argjson private "$private" \ + --arg desc "$desc" \ + '{clone_addr:$addr, service:"gitlab", auth_token:$token, + repo_name:$name, repo_owner:$owner, uid:$uid, + private:$private, description:$desc, + issues:true, pull_requests:true, labels:true, + milestones:true, releases:true, wiki:true}') + + local code + code=$(curl -s -o /tmp/forgejo_migrate_resp.json -w '%{http_code}' \ + -H "Authorization: token ${FORGEJO_TOKEN}" -H "Content-Type: application/json" \ + -X POST "${FORGEJO_URL}/api/v1/repos/migrate" -d "$payload") + + if [ "$code" = "201" ]; then + echo " OK" + elif [ "$code" = "409" ]; then + echo " SKIP (already exists)" + else + echo " FAILED (HTTP $code): $(jq -r '.message // .' /tmp/forgejo_migrate_resp.json)" + fi +} + +# Page through all GitLab projects the token can see. +page=1 +while :; do + resp=$(curl -fsS -H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \ + "${GITLAB_URL}/api/v4/projects?membership=true&per_page=100&page=${page}&simple=false") + count=$(echo "$resp" | jq 'length') + [ "$count" -eq 0 ] && break + + while IFS=$'\t' read -r http_url repo_path visibility description; do + private=true; [ "$visibility" = "public" ] && private=false + # Use the bare repo slug (e.g. "dfg_3dviewer_embed"), dropping the GitLab namespace. + migrate_one "$http_url" "$repo_path" "$private" "${description:-}" + done < <(echo "$resp" | jq -r '.[] | [.http_url_to_repo, .path, .visibility, (.description // "")] | @tsv') + + page=$((page + 1)) +done + +echo "Done." diff --git a/gitlab/docker-compose.yml b/gitlab/docker-compose.yml deleted file mode 100644 index eab4cae..0000000 --- a/gitlab/docker-compose.yml +++ /dev/null @@ -1,54 +0,0 @@ -services: - gitlab: - image: gitlab/gitlab-ce:17.8.6-ce.0 - container_name: gitlab - hostname: '${GITLAB_DOMAIN}' - environment: - GITLAB_OMNIBUS_CONFIG: | - # Add any other gitlab.rb configuration here, each on its own line - external_url 'https://${GITLAB_DOMAIN}' - gitlab_rails['gitlab_shell_ssh_port'] = 2424 - # We need to tell GitLab to use SSH port 22 internally - gitlab_shell['auth_file'] = "/var/opt/gitlab/.ssh/authorized_keys" - #gitlab_shell['ssh_port'] = 22 - nginx['listen_port'] = 80 - nginx['listen_https'] = false - nginx['proxy_set_headers'] = { - "X-Forwarded-Proto" => "https", - "X-Forwarded-Ssl" => "on" - } - labels: - - traefik.enable=true - - traefik.docker.network=traefik - # HTTP configuration - - traefik.http.routers.gitlab.entrypoints=web,websecure - - traefik.http.routers.gitlab.tls=true - - traefik.http.routers.gitlab.tls.certresolver=le - - traefik.http.routers.gitlab.rule=Host(`${GITLAB_DOMAIN}`) - - traefik.http.services.gitlab.loadbalancer.server.port=80 - # TCP/SSH configuration - completely revised - - "traefik.tcp.routers.gitlab-ssh.rule=HostSNI(`*`)" - - "traefik.tcp.routers.gitlab-ssh.entrypoints=gitlab-ssh" - - "traefik.tcp.services.gitlab-ssh.loadbalancer.server.port=22" - volumes: - - 'gitlab-config:/etc/gitlab' - - 'gitlab-logs:/var/log/gitlab' - - 'gitlab-data:/var/opt/gitlab' - shm_size: '256m' - networks: - - traefik - restart: unless-stopped - - -volumes: - gitlab-config: - name: gitlab-config - gitlab-logs: - name: gitlab-logs - gitlab-data: - name: gitlab-data - -networks: - traefik: - name: traefik - external: true diff --git a/hedgedoc/docker-compose.yml b/hedgedoc/docker-compose.yml index 5c4a1cc..5e6a2be 100644 --- a/hedgedoc/docker-compose.yml +++ b/hedgedoc/docker-compose.yml @@ -1,8 +1,9 @@ services: hedgedoc: # Make sure to use the latest release from https://hedgedoc.org/latest-release - image: quay.io/hedgedoc/hedgedoc:1.10.2 + image: quay.io/hedgedoc/hedgedoc:1.10.3 container_name: hedgedoc + restart: unless-stopped environment: - CMD_DB_URL=postgres://${HEDGEDOC_DB_USER}:${HEDGEDOC_DB_PASSWORD}@${HEDGEDOC_DB_HOST}:${HEDGEDOC_DB_PORT}/${HEDGEDOC_DB_NAME} - CMD_DOMAIN=${HEDGEDOC_DOMAIN} diff --git a/mailcow/.gitignore b/mailcow/.gitignore index c225dc0..a06c3da 100644 --- a/mailcow/.gitignore +++ b/mailcow/.gitignore @@ -75,3 +75,4 @@ refresh_images.sh update_diffs/ create_cold_standby.sh !data/conf/nginx/mailcow_auth.conf +data/conf/postfix/postfix-tlspol \ No newline at end of file diff --git a/mailcow/CONTRIBUTING.md b/mailcow/CONTRIBUTING.md index fae8f1d..5e194b2 100644 --- a/mailcow/CONTRIBUTING.md +++ b/mailcow/CONTRIBUTING.md @@ -1,11 +1,11 @@ # Contribution Guidelines -**_Last modified on 15th August 2024_** +**_Last modified on 12th November 2025_** First of all, thank you for wanting to provide a bugfix or a new feature for the mailcow community, it's because of your help that the project can continue to grow! As we want to keep mailcow's development structured we setup these Guidelines which helps you to create your issue/pull request accordingly. -**PLEASE NOTE, THAT WE MIGHT CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULLFIL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request. +**PLEASE NOTE, THAT WE WILL CLOSE ISSUES/PULL REQUESTS IF THEY DON'T FULFILL OUR WRITTEN GUIDELINES WRITTEN INSIDE THIS DOCUMENT**. So please check this guidelines before you propose a Issue/Pull Request. ## Topics @@ -27,14 +27,18 @@ However, please note the following regarding pull requests: 6. Please **ALWAYS** create the actual pull request against the staging branch and **NEVER** directly against the master branch. *If you forget to do this, our moobot will remind you to switch the branch to staging.* 7. Wait for a merge commit: It may happen that we do not accept your pull request immediately or sometimes not at all for various reasons. Please do not be disappointed if this is the case. We always endeavor to incorporate any meaningful changes from the community into the mailcow project. 8. If you are planning larger and therefore more complex pull requests, it would be advisable to first announce this in a separate issue and then start implementing it after the idea has been accepted in order to avoid unnecessary frustration and effort! +9. If your PR requires a Docker image rebuild (changes to Dockerfiles or files in data/Dockerfiles/), update the image tag in docker-compose.yml. Use the base-image versioning (e.g. ghcr.io/mailcow/sogo:5.12.4 → :5.12.5 for version bumps; append a letter for patch fixes, e.g. :5.12.4a). Follow this scheme. --- ## Issue Reporting -**_Last modified on 15th August 2024_** +**_Last modified on 12th November 2025_** If you plan to report a issue within mailcow please read and understand the following rules: +### Security disclosures / Security-related fixes +- Security vulnerabilities and security fixes must always be reported confidentially first to the contact address specified in SECURITY.md before they are integrated, published, or publicly disclosed in issues/PRs. Please wait for a response from the specified contact to ensure coordinated and responsible disclosure. + ### Issue Reporting Guidelines 1. **ONLY** use the issue tracker for bug reports or improvement requests and NOT for support questions. For support questions you can either contact the [mailcow community on Telegram](https://docs.mailcow.email/#community-support-and-chat) or the mailcow team directly in exchange for a [support fee](https://docs.mailcow.email/#commercial-support). diff --git a/mailcow/_modules/scripts/core.sh b/mailcow/_modules/scripts/core.sh new file mode 100644 index 0000000..576967a --- /dev/null +++ b/mailcow/_modules/scripts/core.sh @@ -0,0 +1,230 @@ +#!/usr/bin/env bash +# _modules/scripts/core.sh +# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY! +# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!! + +# ANSI color for red errors +RED='\e[31m' +GREEN='\e[32m' +YELLOW='\e[33m' +BLUE='\e[34m' +MAGENTA='\e[35m' +LIGHT_RED='\e[91m' +LIGHT_GREEN='\e[92m' +NC='\e[0m' + +caller="${BASH_SOURCE[1]##*/}" + +get_installed_tools(){ + for bin in openssl curl docker git awk sha1sum grep cut jq; do + if [[ -z $(command -v ${bin}) ]]; then + echo "Error: Cannot find command '${bin}'. Cannot proceed." + echo "Solution: Please review system requirements and install requirements. Then, re-run the script." + echo "See System Requirements: https://docs.mailcow.email/getstarted/install/" + echo "Exiting..." + exit 1 + fi + done + + if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\"${NC}"; exit 1; fi + # This will also cover sort + if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\"${NC}"; exit 1; fi + if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo -e "${LIGHT_RED}BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\"${NC}"; exit 1; fi +} + +get_docker_version(){ + # Check Docker Version (need at least 24.X) + docker_version=$(docker version --format '{{.Server.Version}}' | cut -d '.' -f 1) +} + +get_compose_type(){ + if docker compose > /dev/null 2>&1; then + if docker compose version --short | grep -e "^[2-9]\." -e "^v[2-9]\." -e "^[1-9][0-9]\." -e "^v[1-9][0-9]\." > /dev/null 2>&1; then + COMPOSE_VERSION=native + COMPOSE_COMMAND="docker compose" + if [[ "$caller" == "update.sh" ]]; then + sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf" + fi + echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m" + echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m" + sleep 2 + echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" + else + echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" + echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi + elif docker-compose > /dev/null 2>&1; then + if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then + if docker-compose version --short | grep -e "^[2-9]\." -e "^[1-9][0-9]\." > /dev/null 2>&1; then + COMPOSE_VERSION=standalone + COMPOSE_COMMAND="docker-compose" + if [[ "$caller" == "update.sh" ]]; then + sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf" + fi + echo -e "\e[33mFound Docker Compose Standalone.\e[0m" + echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m" + sleep 2 + echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m" + else + echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" + echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi + fi + else + echo -e "\e[31mCannot find Docker Compose.\e[0m" + echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + exit 1 + fi +} + +detect_bad_asn() { + echo -e "\e[33mDetecting if your IP is listed on Spamhaus Bad ASN List...\e[0m" + response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email") + if [ "$response" -eq 503 ]; then + if [ -z "$SPAMHAUS_DQS_KEY" ]; then + echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" + echo -e "\e[33mmailcow did not detected a value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf!\e[0m" + sleep 2 + echo "" + echo -e "\e[33mTo use the Spamhaus DNS Blocklists again, you will need to create a FREE account for their Data Query Service (DQS) at: https://www.spamhaus.com/free-trial/sign-up-for-a-free-data-query-service-account\e[0m" + echo -e "\e[33mOnce done, enter your DQS API key in mailcow.conf and mailcow will do the rest for you!\e[0m" + echo "" + sleep 2 + else + echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" + echo -e "\e[32mmailcow detected a Value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf. Postfix will use DQS with the given API key...\e[0m" + fi + elif [ "$response" -eq 200 ]; then + echo -e "\e[33mCheck completed! Your IP is \e[32mclean\e[0m" + elif [ "$response" -eq 429 ]; then + echo -e "\e[33mCheck completed! \e[31mYour IP seems to be rate limited on the ASN Check service... please try again later!\e[0m" + else + echo -e "\e[31mCheck failed! \e[0mMaybe a DNS or Network problem?\e[0m" + fi +} + +check_online_status() { + CHECK_ONLINE_DOMAINS=('https://github.com' 'https://hub.docker.com') + for domain in "${CHECK_ONLINE_DOMAINS[@]}"; do + if timeout 6 curl --head --silent --output /dev/null ${domain}; then + return 0 + fi + done + return 1 +} + +prefetch_images() { + [[ -z ${BRANCH} ]] && { echo -e "\e[33m\nUnknown branch...\e[0m"; exit 1; } + git fetch origin #${BRANCH} + while read image; do + RET_C=0 + until docker pull "${image}"; do + RET_C=$((RET_C + 1)) + echo -e "\e[33m\nError pulling $image, retrying...\e[0m" + [ ${RET_C} -gt 3 ] && { echo -e "\e[31m\nToo many failed retries, exiting\e[0m"; exit 1; } + sleep 1 + done + done < <(git show "origin/${BRANCH}:docker-compose.yml" | grep "image:" | awk '{ gsub("image:","", $3); print $2 }') +} + +docker_garbage() { + SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." && pwd )" + IMGS_TO_DELETE=() + + declare -A IMAGES_INFO + COMPOSE_IMAGES=($(grep -oP "image: \K(ghcr\.io/)?mailcow.+" "${SCRIPT_DIR}/docker-compose.yml")) + + for existing_image in $(docker images --format "{{.ID}}:{{.Repository}}:{{.Tag}}" | grep -E '(mailcow/|ghcr\.io/mailcow/)'); do + ID=$(echo "$existing_image" | cut -d ':' -f 1) + REPOSITORY=$(echo "$existing_image" | cut -d ':' -f 2) + TAG=$(echo "$existing_image" | cut -d ':' -f 3) + + if [[ "$REPOSITORY" == "mailcow/backup" || "$REPOSITORY" == "ghcr.io/mailcow/backup" ]]; then + if [[ "$TAG" != "" ]]; then + continue + fi + fi + + if [[ " ${COMPOSE_IMAGES[@]} " =~ " ${REPOSITORY}:${TAG} " ]]; then + continue + else + IMGS_TO_DELETE+=("$ID") + IMAGES_INFO["$ID"]="$REPOSITORY:$TAG" + fi + done + + if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then + echo "The following unused mailcow images were found:" + for id in "${IMGS_TO_DELETE[@]}"; do + echo " ${IMAGES_INFO[$id]} ($id)" + done + + if [ -z "$FORCE" ]; then + read -r -p "Do you want to delete them to free up some space? [y/N] " response + if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + docker rmi ${IMGS_TO_DELETE[*]} + else + echo "OK, skipped." + fi + else + echo "Running in forced mode! Force removing old mailcow images..." + docker rmi ${IMGS_TO_DELETE[*]} + fi + echo -e "\e[32mFurther cleanup...\e[0m" + echo "If you want to cleanup further garbage collected by Docker, please make sure all containers are up and running before cleaning your system by executing \"docker system prune\"" + fi +} + +in_array() { + local e match="$1" + shift + for e; do [[ "$e" == "$match" ]] && return 0; done + return 1 +} + +detect_major_update() { + if [ ${BRANCH} == "master" ]; then + # Array with major versions + # Add major versions here + MAJOR_VERSIONS=( + "2025-02" + "2025-03" + "2025-09" + ) + + current_version="" + if [[ -f "${SCRIPT_DIR}/data/web/inc/app_info.inc.php" ]]; then + current_version=$(grep 'MAILCOW_GIT_VERSION' ${SCRIPT_DIR}/data/web/inc/app_info.inc.php | sed -E 's/.*MAILCOW_GIT_VERSION="([^"]+)".*/\1/') + fi + if [[ -z "$current_version" ]]; then + return 1 + fi + release_url="https://github.com/mailcow/mailcow-dockerized/releases/tag" + + updates_to_apply=() + + for version in "${MAJOR_VERSIONS[@]}"; do + if [[ "$current_version" < "$version" ]]; then + updates_to_apply+=("$version") + fi + done + + if [[ ${#updates_to_apply[@]} -gt 0 ]]; then + echo -e "\e[33m\nMAJOR UPDATES to be applied:\e[0m" + for update in "${updates_to_apply[@]}"; do + echo "$update - $release_url/$update" + done + + echo -e "\nPlease read the release notes before proceeding." + read -p "Do you want to proceed with the update? [y/n] " response + if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "Proceeding with the update..." + else + echo "Update canceled. Exiting." + exit 1 + fi + fi + fi +} diff --git a/mailcow/_modules/scripts/ipv6_controller.sh b/mailcow/_modules/scripts/ipv6_controller.sh new file mode 100644 index 0000000..a025ee1 --- /dev/null +++ b/mailcow/_modules/scripts/ipv6_controller.sh @@ -0,0 +1,239 @@ +#!/usr/bin/env bash +# _modules/scripts/ipv6_controller.sh +# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY! +# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!! + +# 1) Check if the host supports IPv6 +get_ipv6_support() { + # ---- helper: probe external IPv6 connectivity without DNS ---- + _probe_ipv6_connectivity() { + # Use literal, always-on IPv6 echo responders (no DNS required) + local PROBE_IPS=("2001:4860:4860::8888" "2606:4700:4700::1111") + local ip rc=1 + + for ip in "${PROBE_IPS[@]}"; do + if command -v ping6 &>/dev/null; then + ping6 -c1 -W2 "$ip" &>/dev/null || ping6 -c1 -w2 "$ip" &>/dev/null + rc=$? + elif command -v ping &>/dev/null; then + ping -6 -c1 -W2 "$ip" &>/dev/null || ping -6 -c1 -w2 "$ip" &>/dev/null + rc=$? + else + rc=1 + fi + [[ $rc -eq 0 ]] && return 0 + done + return 1 + } + + if [[ ! -f /proc/net/if_inet6 ]] || grep -qs '^1' /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null; then + DETECTED_IPV6=false + echo -e "${YELLOW}IPv6 not detected on host – ${LIGHT_RED}IPv6 is administratively disabled${YELLOW}.${NC}" + return + fi + + if ip -6 route show default 2>/dev/null | grep -qE '^default'; then + echo -e "${YELLOW}Default IPv6 route found – testing external IPv6 connectivity...${NC}" + if _probe_ipv6_connectivity; then + DETECTED_IPV6=true + echo -e "IPv6 detected on host – ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}" + else + DETECTED_IPV6=false + echo -e "${YELLOW}Default IPv6 route present but external IPv6 connectivity failed – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}" + fi + return + fi + + if ip -6 addr show scope global 2>/dev/null | grep -q 'inet6'; then + DETECTED_IPV6=false + echo -e "${YELLOW}Global IPv6 address present but no default route – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}" + return + fi + + if ip -6 addr show scope link 2>/dev/null | grep -q 'inet6'; then + echo -e "${YELLOW}Only link-local IPv6 addresses found – testing external IPv6 connectivity...${NC}" + if _probe_ipv6_connectivity; then + DETECTED_IPV6=true + echo -e "External IPv6 connectivity available – ${LIGHT_GREEN}leaving IPv6 support enabled${YELLOW}.${NC}" + else + DETECTED_IPV6=false + echo -e "${YELLOW}Only link-local IPv6 present and no external connectivity – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}" + fi + return + fi + + DETECTED_IPV6=false + echo -e "${YELLOW}IPv6 not detected on host – ${LIGHT_RED}disabling IPv6 support${YELLOW}.${NC}" +} + +# 2) Ensure Docker daemon.json has (or create) the required IPv6 settings +docker_daemon_edit(){ + DOCKER_DAEMON_CONFIG="/etc/docker/daemon.json" + DOCKER_MAJOR=$(docker version --format '{{.Server.Version}}' 2>/dev/null | cut -d. -f1) + MISSING=() + + _has_kv() { grep -Eq "\"$1\"[[:space:]]*:[[:space:]]*$2" "$DOCKER_DAEMON_CONFIG" 2>/dev/null; } + + if [[ -f "$DOCKER_DAEMON_CONFIG" ]]; then + + # reject empty or whitespace-only file immediately + if [[ ! -s "$DOCKER_DAEMON_CONFIG" ]] || ! grep -Eq '[{}]' "$DOCKER_DAEMON_CONFIG"; then + echo -e "${RED}ERROR: $DOCKER_DAEMON_CONFIG exists but is empty or contains no JSON braces – please initialize it with valid JSON (e.g. {}).${NC}" + exit 1 + fi + + # Validate JSON if jq is present + if command -v jq &>/dev/null && ! jq empty "$DOCKER_DAEMON_CONFIG" &>/dev/null; then + echo -e "${RED}ERROR: Invalid JSON in $DOCKER_DAEMON_CONFIG – please correct manually.${NC}" + exit 1 + fi + + # Gather missing keys + ! _has_kv ipv6 true && MISSING+=("ipv6: true") + + # For Docker < 28, keep requiring fixed-cidr-v6 (default bridge needs it on old engines) + if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then + ! grep -Eq '"fixed-cidr-v6"[[:space:]]*:[[:space:]]*".+"' "$DOCKER_DAEMON_CONFIG" \ + && MISSING+=('fixed-cidr-v6: "fd00:dead:beef:c0::/80"') + fi + + # For Docker < 27, ip6tables needed and was tied to experimental in older releases + if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then + _has_kv ipv6 true && ! _has_kv ip6tables true && MISSING+=("ip6tables: true") + ! _has_kv experimental true && MISSING+=("experimental: true") + fi + + # Fix if needed + if ((${#MISSING[@]}>0)); then + echo -e "${MAGENTA}Your daemon.json is missing: ${YELLOW}${MISSING[*]}${NC}" + if [[ -n "$FORCE" ]]; then + ans=Y + else + read -p "Would you like to update $DOCKER_DAEMON_CONFIG now? [Y/n] " ans + ans=${ans:-Y} + fi + + if [[ $ans =~ ^[Yy]$ ]]; then + cp "$DOCKER_DAEMON_CONFIG" "${DOCKER_DAEMON_CONFIG}.bak" + if command -v jq &>/dev/null; then + TMP=$(mktemp) + # Base filter: ensure ipv6 = true + JQ_FILTER='.ipv6 = true' + + # Add fixed-cidr-v6 only for Docker < 28 + if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 28 ]]; then + JQ_FILTER+=' | .["fixed-cidr-v6"] = (.["fixed-cidr-v6"] // "fd00:dead:beef:c0::/80")' + fi + + # Add ip6tables/experimental only for Docker < 27 + if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then + JQ_FILTER+=' | .ip6tables = true | .experimental = true' + fi + + jq "$JQ_FILTER" "$DOCKER_DAEMON_CONFIG" >"$TMP" && mv "$TMP" "$DOCKER_DAEMON_CONFIG" + echo -e "${LIGHT_GREEN}daemon.json updated. Restarting Docker...${NC}" + (command -v systemctl &>/dev/null && systemctl restart docker) || service docker restart + echo -e "${YELLOW}Docker restarted.${NC}" + else + echo -e "${RED}Please install jq or manually update daemon.json and restart Docker.${NC}" + exit 1 + fi + else + echo -e "${YELLOW}User declined Docker update – please insert these changes manually:${NC}" + echo "${MISSING[*]}" + exit 1 + fi + fi + + else + # Create new daemon.json if missing + if [[ -n "$FORCE" ]]; then + ans=Y + else + read -p "$DOCKER_DAEMON_CONFIG not found. Create it with IPv6 settings? [Y/n] " ans + ans=${ans:-Y} + fi + + if [[ $ans =~ ^[Yy]$ ]]; then + mkdir -p "$(dirname "$DOCKER_DAEMON_CONFIG")" + if [[ -n "$DOCKER_MAJOR" && "$DOCKER_MAJOR" -lt 27 ]]; then + cat > "$DOCKER_DAEMON_CONFIG" < "$DOCKER_DAEMON_CONFIG" < "$DOCKER_DAEMON_CONFIG" </dev/null && systemctl restart docker) || service docker restart + echo "Docker restarted." + else + echo "User declined to create daemon.json – please manually merge the docker daemon with these configs:" + echo "${MISSING[*]}" + exit 1 + fi + fi +} + +# 3) Main wrapper for generate_config.sh and update.sh +configure_ipv6() { + # detect manual override if mailcow.conf is present + if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]] && grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then + MANUAL_SETTING=$(grep '^ENABLE_IPV6=' "$MAILCOW_CONF" | cut -d= -f2) + elif [[ -z "$MAILCOW_CONF" ]] && [[ -n "${ENABLE_IPV6:-}" ]]; then + MANUAL_SETTING="$ENABLE_IPV6" + else + MANUAL_SETTING="" + fi + + get_ipv6_support + + # if user manually set it, check for mismatch + if [[ "$DETECTED_IPV6" != "true" ]]; then + if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then + if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then + sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=false/' "$MAILCOW_CONF" + else + echo "ENABLE_IPV6=false" >> "$MAILCOW_CONF" + fi + else + export IPV6_BOOL=false + fi + echo "Skipping Docker IPv6 configuration because host does not support IPv6." + echo "Make sure to check if your docker daemon.json does not include \"enable_ipv6\": true if you do not want IPv6." + echo "IPv6 configuration complete: ENABLE_IPV6=false" + sleep 2 + return + fi + + docker_daemon_edit + + if [[ -n "$MAILCOW_CONF" && -f "$MAILCOW_CONF" ]]; then + if grep -q '^ENABLE_IPV6=' "$MAILCOW_CONF"; then + sed -i 's/^ENABLE_IPV6=.*/ENABLE_IPV6=true/' "$MAILCOW_CONF" + else + echo "ENABLE_IPV6=true" >> "$MAILCOW_CONF" + fi + else + export IPV6_BOOL=true + fi + + echo "IPv6 configuration complete: ENABLE_IPV6=true" +} \ No newline at end of file diff --git a/mailcow/_modules/scripts/migrate_options.sh b/mailcow/_modules/scripts/migrate_options.sh new file mode 100644 index 0000000..6a584b9 --- /dev/null +++ b/mailcow/_modules/scripts/migrate_options.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# _modules/scripts/migrate_options.sh +# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY! +# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!! + +migrate_config_options() { + + sed -i --follow-symlinks '$a\' mailcow.conf + + KEYS=( + SOLR_HEAP + SKIP_SOLR + SOLR_PORT + FLATCURVE_EXPERIMENTAL + DISABLE_IPv6 + ACME_CONTACT + ) + + for key in "${KEYS[@]}"; do + if grep -q "${key}" mailcow.conf; then + case "${key}" in + SOLR_HEAP) + echo "Removing ${key} in mailcow.conf" + sed -i '/# Solr heap size in MB\b/d' mailcow.conf + sed -i '/# Solr is a prone to run\b/d' mailcow.conf + sed -i '/SOLR_HEAP\b/d' mailcow.conf + ;; + SKIP_SOLR) + echo "Removing ${key} in mailcow.conf" + sed -i '/\bSkip Solr on low-memory\b/d' mailcow.conf + sed -i '/\bSolr is disabled by default\b/d' mailcow.conf + sed -i '/\bDisable Solr or\b/d' mailcow.conf + sed -i '/\bSKIP_SOLR\b/d' mailcow.conf + ;; + SOLR_PORT) + echo "Removing ${key} in mailcow.conf" + sed -i '/\bSOLR_PORT\b/d' mailcow.conf + ;; + FLATCURVE_EXPERIMENTAL) + echo "Removing ${key} in mailcow.conf" + sed -i '/\bFLATCURVE_EXPERIMENTAL\b/d' mailcow.conf + ;; + DISABLE_IPv6) + echo "Migrating ${key} to ENABLE_IPv6 in mailcow.conf" + local old=$(grep '^DISABLE_IPv6=' "mailcow.conf" | cut -d'=' -f2) + local new + if [[ "$old" == "y" ]]; then + new="false" + else + new="true" + fi + sed -i '/^DISABLE_IPv6=/d' "mailcow.conf" + echo "ENABLE_IPV6=$new" >> "mailcow.conf" + ;; + ACME_CONTACT) + echo "Deleting obsoleted ${key} in mailcow.conf" + sed -i '/^# Lets Encrypt registration contact information/d' mailcow.conf + sed -i '/^# Optional: Leave empty for none/d' mailcow.conf + sed -i '/^# This value is only used on first order!/d' mailcow.conf + sed -i '/^# Setting it at a later point will require the following steps:/d' mailcow.conf + sed -i '/^# https:\/\/docs.mailcow.email\/troubleshooting\/debug-reset_tls\//d' mailcow.conf + sed -i '/^ACME_CONTACT=.*/d' mailcow.conf + sed -i '/^#ACME_CONTACT=.*/d' mailcow.conf + ;; + esac + fi + done + + solr_volume=$(docker volume ls -qf name=^${COMPOSE_PROJECT_NAME}_solr-vol-1) + if [[ -n $solr_volume ]]; then + echo -e "\e[34mSolr has been replaced within mailcow since 2025-01.\nThe volume $solr_volume is unused.\e[0m" + sleep 1 + if [ ! "$FORCE" ]; then + read -r -p "Remove $solr_volume? [y/N] " response + if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo -e "\e[33mRemoving $solr_volume...\e[0m" + docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m" + echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m" + else + echo -e "Not removing $solr_volume. Run \`docker volume rm $solr_volume\` manually if needed." + fi + else + echo -e "\e[33mForce removing $solr_volume...\e[0m" + docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m" + echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m" + fi + fi + + # Delete old fts.conf before forced switch to flatcurve to ensure update is working properly + FTS_CONF_PATH="${SCRIPT_DIR}/data/conf/dovecot/conf.d/fts.conf" + if [[ -f "$FTS_CONF_PATH" ]]; then + if grep -q "Autogenerated by mailcow" "$FTS_CONF_PATH"; then + rm -rf $FTS_CONF_PATH + fi + fi +} \ No newline at end of file diff --git a/mailcow/_modules/scripts/new_options.sh b/mailcow/_modules/scripts/new_options.sh new file mode 100644 index 0000000..30c747b --- /dev/null +++ b/mailcow/_modules/scripts/new_options.sh @@ -0,0 +1,300 @@ +#!/usr/bin/env bash +# _modules/scripts/new_options.sh +# THIS SCRIPT IS DESIGNED TO BE RUNNING BY MAILCOW SCRIPTS ONLY! +# DO NOT, AGAIN, NOT TRY TO RUN THIS SCRIPT STANDALONE!!!!!! + +adapt_new_options() { + + CONFIG_ARRAY=( + "AUTODISCOVER_SAN" + "SKIP_LETS_ENCRYPT" + "SKIP_SOGO" + "USE_WATCHDOG" + "WATCHDOG_NOTIFY_EMAIL" + "WATCHDOG_NOTIFY_WEBHOOK" + "WATCHDOG_NOTIFY_WEBHOOK_BODY" + "WATCHDOG_NOTIFY_BAN" + "WATCHDOG_NOTIFY_START" + "WATCHDOG_EXTERNAL_CHECKS" + "WATCHDOG_SUBJECT" + "SKIP_CLAMD" + "SKIP_OLEFY" + "SKIP_IP_CHECK" + "ADDITIONAL_SAN" + "DOVEADM_PORT" + "IPV4_NETWORK" + "IPV6_NETWORK" + "LOG_LINES" + "SNAT_TO_SOURCE" + "SNAT6_TO_SOURCE" + "COMPOSE_PROJECT_NAME" + "DOCKER_COMPOSE_VERSION" + "SQL_PORT" + "API_KEY" + "API_KEY_READ_ONLY" + "API_ALLOW_FROM" + "MAILDIR_GC_TIME" + "MAILDIR_SUB" + "ACL_ANYONE" + "FTS_HEAP" + "FTS_PROCS" + "SKIP_FTS" + "ENABLE_SSL_SNI" + "ALLOW_ADMIN_EMAIL_LOGIN" + "SKIP_HTTP_VERIFICATION" + "SOGO_EXPIRE_SESSION" + "SOGO_URL_ENCRYPTION_KEY" + "REDIS_PORT" + "REDISPASS" + "DOVECOT_MASTER_USER" + "DOVECOT_MASTER_PASS" + "MAILCOW_PASS_SCHEME" + "ADDITIONAL_SERVER_NAMES" + "WATCHDOG_VERBOSE" + "WEBAUTHN_ONLY_TRUSTED_VENDORS" + "SPAMHAUS_DQS_KEY" + "SKIP_UNBOUND_HEALTHCHECK" + "DISABLE_NETFILTER_ISOLATION_RULE" + "HTTP_REDIRECT" + "ENABLE_IPV6" + ) + + sed -i --follow-symlinks '$a\' mailcow.conf + for option in ${CONFIG_ARRAY[@]}; do + if grep -q "${option}" mailcow.conf; then + continue + fi + + echo "Adding new option \"${option}\" to mailcow.conf" + + case "${option}" in + AUTODISCOVER_SAN) + echo '# Obtain certificates for autodiscover.* and autoconfig.* domains.' >> mailcow.conf + echo '# This can be useful to switch off in case you are in a scenario where a reverse proxy already handles those.' >> mailcow.conf + echo '# There are mixed scenarios where ports 80,443 are occupied and you do not want to share certs' >> mailcow.conf + echo '# between services. So acme-mailcow obtains for maildomains and all web-things get handled' >> mailcow.conf + echo '# in the reverse proxy.' >> mailcow.conf + echo 'AUTODISCOVER_SAN=y' >> mailcow.conf + ;; + + DOCKER_COMPOSE_VERSION) + echo "# Used Docker Compose version" >> mailcow.conf + echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf + echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf + echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf + echo "# Please be aware that at least one of those variants should be installed on your machine or mailcow will fail." >> mailcow.conf + echo "" >> mailcow.conf + echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf + ;; + + DOVEADM_PORT) + echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf + ;; + + LOG_LINES) + echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf + echo "LOG_LINES=9999" >> mailcow.conf + ;; + IPV4_NETWORK) + echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf + echo "IPV4_NETWORK=172.22.1" >> mailcow.conf + ;; + IPV6_NETWORK) + echo '# Internal IPv6 subnet in fc00::/7' >> mailcow.conf + echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf + ;; + SQL_PORT) + echo '# Bind SQL to 127.0.0.1 on port 13306' >> mailcow.conf + echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf + ;; + API_KEY) + echo '# Create or override API key for web UI' >> mailcow.conf + echo "#API_KEY=" >> mailcow.conf + ;; + API_KEY_READ_ONLY) + echo '# Create or override read-only API key for web UI' >> mailcow.conf + echo "#API_KEY_READ_ONLY=" >> mailcow.conf + ;; + API_ALLOW_FROM) + echo '# Must be set for API_KEY to be active' >> mailcow.conf + echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf + echo "#API_ALLOW_FROM=" >> mailcow.conf + ;; + SNAT_TO_SOURCE) + echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf + echo "#SNAT_TO_SOURCE=" >> mailcow.conf + ;; + SNAT6_TO_SOURCE) + echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf + echo "#SNAT6_TO_SOURCE=" >> mailcow.conf + ;; + MAILDIR_GC_TIME) + echo '# Garbage collector cleanup' >> mailcow.conf + echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf + echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf + echo '# Check interval is hourly' >> mailcow.conf + echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf + ;; + ACL_ANYONE) + echo '# Set this to "allow" to enable the anyone pseudo user. Disabled by default.' >> mailcow.conf + echo '# When enabled, ACL can be created, that apply to "All authenticated users"' >> mailcow.conf + echo '# This should probably only be activated on mail hosts, that are used exclusively by one organisation.' >> mailcow.conf + echo '# Otherwise a user might share data with too many other users.' >> mailcow.conf + echo 'ACL_ANYONE=disallow' >> mailcow.conf + ;; + FTS_HEAP) + echo '# Dovecot Indexing (FTS) Process maximum heap size in MB, there is no recommendation, please see Dovecot docs.' >> mailcow.conf + echo '# Flatcurve is used as FTS Engine. It is supposed to be pretty efficient in CPU and RAM consumption.' >> mailcow.conf + echo '# Please always monitor your Resource consumption!' >> mailcow.conf + echo "FTS_HEAP=128" >> mailcow.conf + ;; + SKIP_FTS) + echo '# Skip FTS (Fulltext Search) for Dovecot on low-memory, low-threaded systems or if you simply want to disable it.' >> mailcow.conf + echo "# Dovecot inside mailcow use Flatcurve as FTS Backend." >> mailcow.conf + echo "SKIP_FTS=y" >> mailcow.conf + ;; + FTS_PROCS) + echo '# Controls how many processes the Dovecot indexing process can spawn at max.' >> mailcow.conf + echo '# Too many indexing processes can use a lot of CPU and Disk I/O' >> mailcow.conf + echo '# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more informations' >> mailcow.conf + echo "FTS_PROCS=1" >> mailcow.conf + ;; + ENABLE_SSL_SNI) + echo '# Create seperate certificates for all domains - y/n' >> mailcow.conf + echo '# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames' >> mailcow.conf + echo '# see https://wiki.dovecot.org/SSL/SNIClientSupport' >> mailcow.conf + echo "ENABLE_SSL_SNI=n" >> mailcow.conf + ;; + SKIP_SOGO) + echo '# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n' >> mailcow.conf + echo "SKIP_SOGO=n" >> mailcow.conf + ;; + MAILDIR_SUB) + echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf + echo "#MAILDIR_SUB=Maildir" >> mailcow.conf + echo "MAILDIR_SUB=" >> mailcow.conf + ;; + WATCHDOG_NOTIFY_WEBHOOK) + echo '# Send notifications to a webhook URL that receives a POST request with the content type "application/json".' >> mailcow.conf + echo '# You can use this to send notifications to services like Discord, Slack and others.' >> mailcow.conf + echo '#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' >> mailcow.conf + ;; + WATCHDOG_NOTIFY_WEBHOOK_BODY) + echo '# JSON body included in the webhook POST request. Needs to be in single quotes.' >> mailcow.conf + echo '# Following variables are available: SUBJECT, BODY' >> mailcow.conf + WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}' + echo "#WATCHDOG_NOTIFY_WEBHOOK_BODY='${WEBHOOK_BODY}'" >> mailcow.conf + ;; + WATCHDOG_NOTIFY_BAN) + echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf + echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf + ;; + WATCHDOG_NOTIFY_START) + echo '# Send a notification when the watchdog is started.' >> mailcow.conf + echo "WATCHDOG_NOTIFY_START=y" >> mailcow.conf + ;; + WATCHDOG_SUBJECT) + echo '# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.' >> mailcow.conf + echo "#WATCHDOG_SUBJECT=" >> mailcow.conf + ;; + WATCHDOG_EXTERNAL_CHECKS) + echo '# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.' >> mailcow.conf + echo '# No data is collected. Opt-in and anonymous.' >> mailcow.conf + echo '# Will only work with unmodified mailcow setups.' >> mailcow.conf + echo "WATCHDOG_EXTERNAL_CHECKS=n" >> mailcow.conf + ;; + SOGO_EXPIRE_SESSION) + echo '# SOGo session timeout in minutes' >> mailcow.conf + echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf + ;; + REDIS_PORT) + echo "REDIS_PORT=127.0.0.1:7654" >> mailcow.conf + ;; + DOVECOT_MASTER_USER) + echo '# DOVECOT_MASTER_USER and _PASS must _both_ be provided. No special chars.' >> mailcow.conf + echo '# Empty by default to auto-generate master user and password on start.' >> mailcow.conf + echo '# User expands to DOVECOT_MASTER_USER@mailcow.local' >> mailcow.conf + echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf + echo "DOVECOT_MASTER_USER=" >> mailcow.conf + ;; + DOVECOT_MASTER_PASS) + echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf + echo "DOVECOT_MASTER_PASS=" >> mailcow.conf + ;; + MAILCOW_PASS_SCHEME) + echo '# Password hash algorithm' >> mailcow.conf + echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf + echo '# see https://docs.mailcow.email/models/model-passwd/' >> mailcow.conf + echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf + ;; + ADDITIONAL_SERVER_NAMES) + echo '# Additional server names for mailcow UI' >> mailcow.conf + echo '#' >> mailcow.conf + echo '# Specify alternative addresses for the mailcow UI to respond to' >> mailcow.conf + echo '# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.' >> mailcow.conf + echo '# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.' >> mailcow.conf + echo '# You can understand this as server_name directive in Nginx.' >> mailcow.conf + echo '# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f' >> mailcow.conf + echo 'ADDITIONAL_SERVER_NAMES=' >> mailcow.conf + ;; + WEBAUTHN_ONLY_TRUSTED_VENDORS) + echo "# WebAuthn device manufacturer verification" >> mailcow.conf + echo '# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed' >> mailcow.conf + echo '# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates' >> mailcow.conf + echo 'WEBAUTHN_ONLY_TRUSTED_VENDORS=n' >> mailcow.conf + ;; + SPAMHAUS_DQS_KEY) + echo "# Spamhaus Data Query Service Key" >> mailcow.conf + echo '# Optional: Leave empty for none' >> mailcow.conf + echo '# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist.' >> mailcow.conf + echo '# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS.' >> mailcow.conf + echo '# Otherwise it will work as usual.' >> mailcow.conf + echo 'SPAMHAUS_DQS_KEY=' >> mailcow.conf + ;; + WATCHDOG_VERBOSE) + echo '# Enable watchdog verbose logging' >> mailcow.conf + echo 'WATCHDOG_VERBOSE=n' >> mailcow.conf + ;; + SKIP_UNBOUND_HEALTHCHECK) + echo '# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n' >> mailcow.conf + echo 'SKIP_UNBOUND_HEALTHCHECK=n' >> mailcow.conf + ;; + DISABLE_NETFILTER_ISOLATION_RULE) + echo '# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n' >> mailcow.conf + echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf + echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf + ;; + HTTP_REDIRECT) + echo '# Redirect HTTP connections to HTTPS - y/n' >> mailcow.conf + echo 'HTTP_REDIRECT=n' >> mailcow.conf + ;; + ENABLE_IPV6) + echo '# IPv6 Controller Section' >> mailcow.conf + echo '# This variable controls the usage of IPv6 within mailcow.' >> mailcow.conf + echo '# Can either be true or false | Defaults to true' >> mailcow.conf + echo '# WARNING: MAKE SURE TO PROPERLY CONFIGURE IPv6 ON YOUR HOST FIRST BEFORE ENABLING THIS AS FAULTY CONFIGURATIONS CAN LEAD TO OPEN RELAYS!' >> mailcow.conf + echo '# A COMPLETE DOCKER STACK REBUILD (compose down && compose up -d) IS NEEDED TO APPLY THIS.' >> mailcow.conf + echo ENABLE_IPV6=${IPV6_BOOL} >> mailcow.conf + ;; + SKIP_CLAMD) + echo '# Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n' >> mailcow.conf + echo 'SKIP_CLAMD=n' >> mailcow.conf + ;; + SKIP_OLEFY) + echo '# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n' >> mailcow.conf + echo 'SKIP_OLEFY=n' >> mailcow.conf + ;; + REDISPASS) + echo "REDISPASS=$(LC_ALL=C /dev/null | head -c 28)" >> mailcow.conf + ;; + SOGO_URL_ENCRYPTION_KEY) + echo '# SOGo URL encryption key (exactly 16 characters, limited to A–Z, a–z, 0–9)' >> mailcow.conf + 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 + ;; + *) + echo "${option}=" >> mailcow.conf + ;; + esac + done +} \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/acme/acme.sh b/mailcow/data/Dockerfiles/acme/acme.sh index a6766ef..69b18bc 100755 --- a/mailcow/data/Dockerfiles/acme/acme.sh +++ b/mailcow/data/Dockerfiles/acme/acme.sh @@ -159,18 +159,6 @@ while true; do fi if [[ ! -f ${ACME_BASE}/acme/account.pem ]]; then log_f "Generating missing Lets Encrypt account key..." - if [[ ! -z ${ACME_CONTACT} ]]; then - if ! verify_email "${ACME_CONTACT}"; then - log_f "Invalid email address, will not start registration!" - sleep 365d - exec $(readlink -f "$0") - else - ACME_CONTACT_PARAMETER="--contact mailto:${ACME_CONTACT}" - log_f "Valid email address, using ${ACME_CONTACT} for registration" - fi - else - ACME_CONTACT_PARAMETER="" - fi openssl genrsa 4096 > ${ACME_BASE}/acme/account.pem else log_f "Using existing Lets Encrypt account key ${ACME_BASE}/acme/account.pem" @@ -218,7 +206,7 @@ while true; do if [[ ${AUTODISCOVER_SAN} == "y" ]]; then # Fetch certs for autoconfig and autodiscover subdomains - ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig') + ADDITIONAL_WC_ARR+=('autodiscover' 'autoconfig' 'mta-sts') fi if [[ ${SKIP_IP_CHECK} != "y" ]]; then @@ -299,7 +287,7 @@ while true; do VALIDATED_CERTIFICATES+=("${CERT_NAME}") # obtain server certificate if required - ACME_CONTACT_PARAMETER=${ACME_CONTACT_PARAMETER} DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa + DOMAINS=${SERVER_SAN_VALIDATED[@]} /srv/obtain-certificate.sh rsa RETURN="$?" if [[ "$RETURN" == "0" ]]; then # 0 = cert created successfully CERT_AMOUNT_CHANGED=1 diff --git a/mailcow/data/Dockerfiles/acme/obtain-certificate.sh b/mailcow/data/Dockerfiles/acme/obtain-certificate.sh index 16c4e25..f476bf6 100644 --- a/mailcow/data/Dockerfiles/acme/obtain-certificate.sh +++ b/mailcow/data/Dockerfiles/acme/obtain-certificate.sh @@ -93,8 +93,8 @@ until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do sleep 2 done log_f "Resolver OK" -log_f "Using command acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/" -ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} ${ACME_CONTACT_PARAMETER} \ +log_f "Using command acme-tiny ${DIRECTORY_URL} --account-key ${ACME_BASE}/acme/account.pem --disable-check --csr ${CSR} --acme-dir /var/www/acme/" +ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} \ --account-key ${ACME_BASE}/acme/account.pem \ --disable-check \ --csr ${CSR} \ diff --git a/mailcow/data/Dockerfiles/backup/Dockerfile b/mailcow/data/Dockerfiles/backup/Dockerfile index 6234e72..9ff6bbb 100644 --- a/mailcow/data/Dockerfiles/backup/Dockerfile +++ b/mailcow/data/Dockerfiles/backup/Dockerfile @@ -1,3 +1,3 @@ -FROM debian:bookworm-slim +FROM debian:trixie-slim -RUN apt update && apt install pigz -y --no-install-recommends \ No newline at end of file +RUN apt update && apt install pigz zstd -y --no-install-recommends \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/clamd/clamd.sh b/mailcow/data/Dockerfiles/clamd/clamd.sh index 2c6e75d..c656be0 100755 --- a/mailcow/data/Dockerfiles/clamd/clamd.sh +++ b/mailcow/data/Dockerfiles/clamd/clamd.sh @@ -8,7 +8,7 @@ fi # Cleaning up garbage echo "Cleaning up tmp files..." -rm -rf /var/lib/clamav/clamav-*.tmp +rm -rf /var/lib/clamav/tmp.* # Prepare whitelist diff --git a/mailcow/data/Dockerfiles/dovecot/Dockerfile b/mailcow/data/Dockerfiles/dovecot/Dockerfile index 9e49d88..10e141a 100644 --- a/mailcow/data/Dockerfiles/dovecot/Dockerfile +++ b/mailcow/data/Dockerfiles/dovecot/Dockerfile @@ -3,7 +3,7 @@ FROM alpine:3.21 LABEL maintainer="The Infrastructure Company GmbH " # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?.*)$ -ARG GOSU_VERSION=1.16 +ARG GOSU_VERSION=1.17 ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 diff --git a/mailcow/data/Dockerfiles/dovecot/docker-entrypoint.sh b/mailcow/data/Dockerfiles/dovecot/docker-entrypoint.sh index fe2341b..e58abbb 100755 --- a/mailcow/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/mailcow/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -204,16 +204,17 @@ EOF # Create random master Password for SOGo SSO RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 32 | head -n 1) echo -n ${RAND_PASS} > /etc/phpfpm/sogo-sso.pass -# Creating additional creds file for SOGo notify crons (calendars, etc) -echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds cat < /etc/dovecot/sogo-sso.conf # Autogenerated by mailcow passdb { driver = static - args = allow_real_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS} + args = allow_nets=${IPV4_NETWORK}.248/32 password={plain}${RAND_PASS} } EOF +# Creating additional creds file for SOGo notify crons (calendars, etc) (dummy user, sso password) +echo -n ${RAND_USER}@mailcow.local:${RAND_PASS} > /etc/sogo/cron.creds + if [[ "${MASTER}" =~ ^([nN][oO]|[nN])+$ ]]; then # Toggling MASTER will result in a rebuild of containers, so the quota script will be recreated cat <<'EOF' > /usr/local/bin/quota_notify.py diff --git a/mailcow/data/Dockerfiles/dovecot/imapsync_runner.pl b/mailcow/data/Dockerfiles/dovecot/imapsync_runner.pl index 9eaf5f4..1030603 100644 --- a/mailcow/data/Dockerfiles/dovecot/imapsync_runner.pl +++ b/mailcow/data/Dockerfiles/dovecot/imapsync_runner.pl @@ -132,8 +132,8 @@ while ($row = $sth->fetchrow_arrayref()) { "--tmpdir", "/tmp", "--nofoldersizes", "--addheader", - ($timeout1 gt "0" ? () : ('--timeout1', $timeout1)), - ($timeout2 gt "0" ? () : ('--timeout2', $timeout2)), + ($timeout1 le "0" ? () : ('--timeout1', $timeout1)), + ($timeout2 le "0" ? () : ('--timeout2', $timeout2)), ($exclude eq "" ? () : ("--exclude", $exclude)), ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), ($maxage eq "0" ? () : ('--maxage', $maxage)), diff --git a/mailcow/data/Dockerfiles/dovecot/quarantine_notify.py b/mailcow/data/Dockerfiles/dovecot/quarantine_notify.py index dfcb1f2..a681c1f 100755 --- a/mailcow/data/Dockerfiles/dovecot/quarantine_notify.py +++ b/mailcow/data/Dockerfiles/dovecot/quarantine_notify.py @@ -8,7 +8,8 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import COMMASPACE, formatdate import jinja2 -from jinja2 import Template +from jinja2 import TemplateError +from jinja2.sandbox import SandboxedEnvironment import json import redis import time @@ -75,22 +76,27 @@ try: def notify_rcpt(rcpt, msg_count, quarantine_acl, category): if category == "add_header": category = "add header" - meta_query = query_mysql('SELECT SHA2(CONCAT(id, qid), 256) AS 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 < %f 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 msg_count = len(meta_query) + env = SandboxedEnvironment() if r.get('Q_HTML'): - try: - template = Template(r.get('Q_HTML')) - except: - print("Error: Cannot parse quarantine template, falling back to default template.") - with open('/templates/quarantine.tpl') as file_: - template = Template(file_.read()) + try: + template = env.from_string(r.get('Q_HTML')) + except Exception: + print("Error: Cannot parse quarantine template, falling back to default template.") + with open('/templates/quarantine.tpl') as file_: + template = env.from_string(file_.read()) else: - with open('/templates/quarantine.tpl') as file_: - template = Template(file_.read()) - html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl) + with open('/templates/quarantine.tpl') as file_: + template = env.from_string(file_.read()) + try: + html = template.render(meta=meta_query, username=rcpt, counter=msg_count, hostname=mailcow_hostname, quarantine_acl=quarantine_acl) + except (jinja2.exceptions.SecurityError, TemplateError) as ex: + print(f"SecurityError or TemplateError in template rendering: {ex}") + return text = html2text.html2text(html) count = 0 while count < 15: diff --git a/mailcow/data/Dockerfiles/dovecot/quota_notify.py b/mailcow/data/Dockerfiles/dovecot/quota_notify.py index 598134e..99be7cc 100755 --- a/mailcow/data/Dockerfiles/dovecot/quota_notify.py +++ b/mailcow/data/Dockerfiles/dovecot/quota_notify.py @@ -6,7 +6,7 @@ from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import COMMASPACE, formatdate import jinja2 -from jinja2 import Template +from jinja2.sandbox import SandboxedEnvironment import redis import time import json @@ -33,16 +33,24 @@ while True: if r.get('QW_HTML'): try: - template = Template(r.get('QW_HTML')) - except: - print("Error: Cannot parse quarantine template, falling back to default template.") + env = SandboxedEnvironment() + template = env.from_string(r.get('QW_HTML')) + except Exception: + print("Error: Cannot parse quota template, falling back to default template.") with open('/templates/quota.tpl') as file_: - template = Template(file_.read()) + env = SandboxedEnvironment() + template = env.from_string(file_.read()) else: with open('/templates/quota.tpl') as file_: - template = Template(file_.read()) + env = SandboxedEnvironment() + template = env.from_string(file_.read()) + +try: + html = template.render(username=username, percent=percent) +except (jinja2.exceptions.SecurityError, jinja2.TemplateError) as ex: + print(f"SecurityError or TemplateError in template rendering: {ex}") + sys.exit(1) -html = template.render(username=username, percent=percent) text = html2text.html2text(html) try: diff --git a/mailcow/data/Dockerfiles/netfilter/docker-entrypoint.sh b/mailcow/data/Dockerfiles/netfilter/docker-entrypoint.sh index 47370a1..98cab0a 100755 --- a/mailcow/data/Dockerfiles/netfilter/docker-entrypoint.sh +++ b/mailcow/data/Dockerfiles/netfilter/docker-entrypoint.sh @@ -1,6 +1,6 @@ #!/bin/sh -backend=iptables +backend=nftables nft list table ip filter &>/dev/null nftables_found=$? diff --git a/mailcow/data/Dockerfiles/netfilter/main.py b/mailcow/data/Dockerfiles/netfilter/main.py index 2b332d2..5b718a9 100644 --- a/mailcow/data/Dockerfiles/netfilter/main.py +++ b/mailcow/data/Dockerfiles/netfilter/main.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +DEBUG = False + import re import os import sys @@ -20,10 +22,13 @@ from modules.Logger import Logger from modules.IPTables import IPTables from modules.NFTables import NFTables +def logdebug(msg): + if DEBUG: + logger.logInfo("DEBUG: %s" % msg) -# globals +# Globals WHITELIST = [] -BLACKLIST= [] +BLACKLIST = [] bans = {} quit_now = False exit_code = 0 @@ -33,12 +38,10 @@ r = None pubsub = None clear_before_quit = False - def refreshF2boptions(): global f2boptions global quit_now global exit_code - f2boptions = {} if not r.get('F2B_OPTIONS'): @@ -52,8 +55,9 @@ def refreshF2boptions(): else: try: f2boptions = json.loads(r.get('F2B_OPTIONS')) - except ValueError: - logger.logCrit('Error loading F2B options: F2B_OPTIONS is not json') + except ValueError as e: + logger.logCrit( + 'Error loading F2B options: F2B_OPTIONS is not json. Exception: %s' % e) quit_now = True exit_code = 2 @@ -61,15 +65,15 @@ def refreshF2boptions(): r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False)) def verifyF2boptions(f2boptions): - verifyF2boption(f2boptions,'ban_time', 1800) - verifyF2boption(f2boptions,'max_ban_time', 10000) - verifyF2boption(f2boptions,'ban_time_increment', True) - verifyF2boption(f2boptions,'max_attempts', 10) - verifyF2boption(f2boptions,'retry_window', 600) - verifyF2boption(f2boptions,'netban_ipv4', 32) - verifyF2boption(f2boptions,'netban_ipv6', 128) - verifyF2boption(f2boptions,'banlist_id', str(uuid.uuid4())) - verifyF2boption(f2boptions,'manage_external', 0) + verifyF2boption(f2boptions, 'ban_time', 1800) + verifyF2boption(f2boptions, 'max_ban_time', 10000) + verifyF2boption(f2boptions, 'ban_time_increment', True) + verifyF2boption(f2boptions, 'max_attempts', 10) + verifyF2boption(f2boptions, 'retry_window', 600) + verifyF2boption(f2boptions, 'netban_ipv4', 32) + verifyF2boption(f2boptions, 'netban_ipv6', 128) + verifyF2boption(f2boptions, 'banlist_id', str(uuid.uuid4())) + verifyF2boption(f2boptions, 'manage_external', 0) def verifyF2boption(f2boptions, f2boption, f2bdefault): f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault @@ -111,7 +115,7 @@ def get_ip(address): def ban(address): global f2boptions global lock - + logdebug("ban() called with address=%s" % address) refreshF2boptions() MAX_ATTEMPTS = int(f2boptions['max_attempts']) RETRY_WINDOW = int(f2boptions['retry_window']) @@ -119,31 +123,43 @@ def ban(address): NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6']) ip = get_ip(address) - if not ip: return + if not ip: + logdebug("No valid IP -- skipping ban()") + return address = str(ip) self_network = ipaddress.ip_network(address) with lock: temp_whitelist = set(WHITELIST) - if temp_whitelist: - for wl_key in temp_whitelist: - wl_net = ipaddress.ip_network(wl_key, False) - if wl_net.overlaps(self_network): - logger.logInfo('Address %s is whitelisted by rule %s' % (self_network, wl_net)) - return + logdebug("Checking if %s overlaps with any WHITELIST entries" % self_network) + if temp_whitelist: + for wl_key in temp_whitelist: + wl_net = ipaddress.ip_network(wl_key, False) + logdebug("Checking overlap between %s and %s" % (self_network, wl_net)) + if wl_net.overlaps(self_network): + logger.logInfo( + 'Address %s is allowlisted by rule %s' % (self_network, wl_net)) + return - net = ipaddress.ip_network((address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False) + net = ipaddress.ip_network( + (address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False) net = str(net) + logdebug("Ban net: %s" % net) if not net in bans: bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0} + logdebug("Initing new ban counter for %s" % net) current_attempt = time.time() + logdebug("Current attempt ts=%s, previous: %s, retry_window: %s" % + (current_attempt, bans[net]['last_attempt'], RETRY_WINDOW)) if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW: bans[net]['attempts'] = 0 + logdebug("Ban counter for %s reset as window expired" % net) bans[net]['attempts'] += 1 bans[net]['last_attempt'] = current_attempt + logdebug("%s attempts now %d" % (net, bans[net]['attempts'])) if bans[net]['attempts'] >= MAX_ATTEMPTS: cur_time = int(round(time.time())) @@ -151,34 +167,41 @@ def ban(address): logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 )) if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1: with lock: + logdebug("Calling tables.banIPv4(%s)" % net) tables.banIPv4(net) elif int(f2boptions['manage_external']) != 1: with lock: + logdebug("Calling tables.banIPv6(%s)" % net) tables.banIPv6(net) + logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" % + (net, cur_time + NET_BAN_TIME)) r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME) else: - logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)) + logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % ( + MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net)) def unban(net): global lock - + logdebug("Calling unban() with net=%s" % net) if not net in bans: - logger.logInfo('%s is not banned, skipping unban and deleting from queue (if any)' % net) - r.hdel('F2B_QUEUE_UNBAN', '%s' % net) - return - + logger.logInfo( + '%s is not banned, skipping unban and deleting from queue (if any)' % net) + r.hdel('F2B_QUEUE_UNBAN', '%s' % net) + return logger.logInfo('Unbanning %s' % net) if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network: with lock: + logdebug("Calling tables.unbanIPv4(%s)" % net) tables.unbanIPv4(net) else: with lock: + logdebug("Calling tables.unbanIPv6(%s)" % net) tables.unbanIPv6(net) - r.hdel('F2B_ACTIVE_BANS', '%s' % net) r.hdel('F2B_QUEUE_UNBAN', '%s' % net) if net in bans: + logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net) bans[net]['attempts'] = 0 bans[net]['ban_counter'] += 1 @@ -204,17 +227,19 @@ def permBan(net, unban=False): if is_unbanned: r.hdel('F2B_PERM_BANS', '%s' % net) - logger.logCrit('Removed host/network %s from blacklist' % net) + logger.logCrit('Removed host/network %s from denylist' % net) elif is_banned: r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time()))) - logger.logCrit('Added host/network %s to blacklist' % net) + logger.logCrit('Added host/network %s to denylist' % net) def clear(): global lock logger.logInfo('Clearing all bans') for net in bans.copy(): + logdebug("Unbanning net: %s" % net) unban(net) with lock: + logdebug("Clearing IPv4/IPv6 table") tables.clearIPv4Table() tables.clearIPv6Table() try: @@ -275,21 +300,35 @@ def snat6(snat_target): def autopurge(): global f2boptions - + logdebug("autopurge thread started") while not quit_now: + logdebug("autopurge tick") time.sleep(10) refreshF2boptions() MAX_ATTEMPTS = int(f2boptions['max_attempts']) QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN') + logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN) if QUEUE_UNBAN: for net in QUEUE_UNBAN: + logdebug("Autopurge: unbanning queued net: %s" % net) unban(str(net)) - for net in bans.copy(): - if bans[net]['attempts'] >= MAX_ATTEMPTS: - NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter']) - TIME_SINCE_LAST_ATTEMPT = time.time() - bans[net]['last_attempt'] - if TIME_SINCE_LAST_ATTEMPT > NET_BAN_TIME: - unban(net) + # Only check expiry for actively banned IPs: + active_bans = r.hgetall('F2B_ACTIVE_BANS') + now = time.time() + for net_str, expire_str in active_bans.items(): + logdebug("Checking ban expiry for (actively banned): %s" % net_str) + # Defensive: always process if timer missing or expired + try: + expire = float(expire_str) + except Exception: + logdebug("Invalid expire time for %s; unbanning" % net_str) + unban(net_str) + continue + time_left = expire - now + logdebug("Time left for %s: %.1f seconds" % (net_str, time_left)) + if time_left <= 0: + logdebug("Ban expired for %s" % net_str) + unban(net_str) def mailcowChainOrder(): global lock @@ -359,7 +398,7 @@ def whitelistUpdate(): with lock: if Counter(new_whitelist) != Counter(WHITELIST): WHITELIST = new_whitelist - logger.logInfo('Whitelist was changed, it has %s entries' % len(WHITELIST)) + logger.logInfo('Allowlist was changed, it has %s entries' % len(WHITELIST)) time.sleep(60.0 - ((time.time() - start_time) % 60.0)) def blacklistUpdate(): @@ -375,7 +414,7 @@ def blacklistUpdate(): addban = set(new_blacklist).difference(BLACKLIST) delban = set(BLACKLIST).difference(new_blacklist) BLACKLIST = new_blacklist - logger.logInfo('Blacklist was changed, it has %s entries' % len(BLACKLIST)) + logger.logInfo('Denylist was changed, it has %s entries' % len(BLACKLIST)) if addban: for net in addban: permBan(net=net) @@ -386,42 +425,43 @@ def blacklistUpdate(): def sigterm_quit(signum, frame): global clear_before_quit + logdebug("SIGTERM received, setting clear_before_quit to True and exiting") clear_before_quit = True sys.exit(exit_code) -def berfore_quit(): +def before_quit(): + logdebug("before_quit called, clear_before_quit=%s" % clear_before_quit) if clear_before_quit: clear() if pubsub is not None: pubsub.unsubscribe() - if __name__ == '__main__': - atexit.register(berfore_quit) + logger = Logger() + logdebug("Sys.argv: %s" % sys.argv) + atexit.register(before_quit) signal.signal(signal.SIGTERM, sigterm_quit) - # init Logger - logger = Logger() - - # init backend backend = sys.argv[1] + logdebug("Backend: %s" % backend) if backend == "nftables": logger.logInfo('Using NFTables backend') tables = NFTables(chain_name, logger) else: logger.logInfo('Using IPTables backend') + logger.logWarn( + "DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. " + "Please switch to nftables on your host to ensure complete compatibility." + ) + time.sleep(5) tables = IPTables(chain_name, logger) - # In case a previous session was killed without cleanup clear() - - # Reinit MAILCOW chain - # Is called before threads start, no locking logger.logInfo("Initializing mailcow netfilter chain") tables.initChainIPv4() tables.initChainIPv6() - if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE").lower() in ("y", "yes"): + if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE", "").lower() in ("y", "yes"): logger.logInfo(f"Skipping {chain_name} isolation") else: logger.logInfo(f"Setting {chain_name} isolation") @@ -432,23 +472,28 @@ if __name__ == '__main__': try: redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '') redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '') + logdebug( + "Connecting redis (SLAVEOF_IP:%s, PORT:%s)" % (redis_slaveof_ip, redis_slaveof_port)) if "".__eq__(redis_slaveof_ip): - r = redis.StrictRedis(host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS']) + r = redis.StrictRedis( + host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS']) else: - r = redis.StrictRedis(host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS']) + r = redis.StrictRedis( + host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS']) r.ping() pubsub = r.pubsub() except Exception as ex: - print('%s - trying again in 3 seconds' % (ex)) + logdebug( + 'Redis connection failed: %s - trying again in 3 seconds' % (ex)) time.sleep(3) else: break logger.set_redis(r) + logdebug("Redis connection established, setting up F2B keys") - # rename fail2ban to netfilter if r.exists('F2B_LOG'): + logdebug("Renaming F2B_LOG to NETFILTER_LOG") r.rename('F2B_LOG', 'NETFILTER_LOG') - # clear bans in redis r.delete('F2B_ACTIVE_BANS') r.delete('F2B_PERM_BANS') @@ -463,7 +508,7 @@ if __name__ == '__main__': snat_ip = os.getenv('SNAT_TO_SOURCE') snat_ipo = ipaddress.ip_address(snat_ip) if type(snat_ipo) is ipaddress.IPv4Address: - snat4_thread = Thread(target=snat4,args=(snat_ip,)) + snat4_thread = Thread(target=snat4, args=(snat_ip,)) snat4_thread.daemon = True snat4_thread.start() except ValueError: @@ -499,4 +544,5 @@ if __name__ == '__main__': while not quit_now: time.sleep(0.5) - sys.exit(exit_code) + logdebug("Exiting with code %s" % exit_code) + sys.exit(exit_code) \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/netfilter/modules/Logger.py b/mailcow/data/Dockerfiles/netfilter/modules/Logger.py index 0ba2f42..b511453 100644 --- a/mailcow/data/Dockerfiles/netfilter/modules/Logger.py +++ b/mailcow/data/Dockerfiles/netfilter/modules/Logger.py @@ -1,5 +1,6 @@ import time import json +import datetime class Logger: def __init__(self): @@ -8,17 +9,28 @@ class Logger: def set_redis(self, redis): self.r = redis + def _format_timestamp(self): + # Local time with milliseconds + return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + def log(self, priority, message): - tolog = {} - tolog['time'] = int(round(time.time())) - tolog['priority'] = priority - tolog['message'] = message - print(message) + # build redis-friendly dict + tolog = { + 'time': int(round(time.time())), # keep raw timestamp for Redis + 'priority': priority, + 'message': message + } + + # print human-readable message with timestamp + ts = self._format_timestamp() + print(f"{ts} {priority.upper()}: {message}", flush=True) + + # also push JSON to Redis if connected if self.r is not None: try: self.r.lpush('NETFILTER_LOG', json.dumps(tolog, ensure_ascii=False)) except Exception as ex: - print('Failed logging to redis: %s' % (ex)) + print(f'{ts} WARN: Failed logging to redis: {ex}', flush=True) def logWarn(self, message): self.log('warn', message) @@ -27,4 +39,4 @@ class Logger: self.log('crit', message) def logInfo(self, message): - self.log('info', message) + self.log('info', message) \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/nginx/bootstrap.py b/mailcow/data/Dockerfiles/nginx/bootstrap.py index 11e6fc2..97ce7a4 100644 --- a/mailcow/data/Dockerfiles/nginx/bootstrap.py +++ b/mailcow/data/Dockerfiles/nginx/bootstrap.py @@ -10,7 +10,7 @@ def includes_conf(env, template_vars): server_name_config = f"server_name {template_vars['MAILCOW_HOSTNAME']} autodiscover.* autoconfig.* {' '.join(template_vars['ADDITIONAL_SERVER_NAMES'])};" listen_plain_config = f"listen {template_vars['HTTP_PORT']};" listen_ssl_config = f"listen {template_vars['HTTPS_PORT']};" - if not template_vars['DISABLE_IPv6']: + if template_vars['ENABLE_IPV6']: listen_plain_config += f"\nlisten [::]:{template_vars['HTTP_PORT']};" listen_ssl_config += f"\nlisten [::]:{template_vars['HTTPS_PORT']} ssl;" listen_ssl_config += "\nhttp2 on;" @@ -58,7 +58,7 @@ def prepare_template_vars(): 'SOGOHOST': os.getenv("SOGOHOST", ipv4_network + ".248"), 'RSPAMDHOST': os.getenv("RSPAMDHOST", "rspamd-mailcow"), 'PHPFPMHOST': os.getenv("PHPFPMHOST", "php-fpm-mailcow"), - 'DISABLE_IPv6': os.getenv("DISABLE_IPv6", "n").lower() in ("y", "yes"), + 'ENABLE_IPV6': os.getenv("ENABLE_IPV6", "true").lower() != "false", 'HTTP_REDIRECT': os.getenv("HTTP_REDIRECT", "n").lower() in ("y", "yes"), } diff --git a/mailcow/data/Dockerfiles/olefy/olefy.py b/mailcow/data/Dockerfiles/olefy/olefy.py index 776e786..7c68809 100644 --- a/mailcow/data/Dockerfiles/olefy/olefy.py +++ b/mailcow/data/Dockerfiles/olefy/olefy.py @@ -32,6 +32,13 @@ import time import magic import re +skip_olefy = os.getenv('SKIP_OLEFY', '') + +if skip_olefy.lower() in ['yes', 'y']: + print("SKIP_OLEFY=y, skipping Olefy...") + time.sleep(365 * 24 * 60 * 60) + sys.exit(0) + # merge variables from /etc/olefy.conf and the defaults olefy_listen_addr_string = os.getenv('OLEFY_BINDADDRESS', '127.0.0.1,::1') olefy_listen_port = int(os.getenv('OLEFY_BINDPORT', '10050')) @@ -113,7 +120,7 @@ def oletools( stream, tmp_file_name, lid ): out = bytes(out.decode('utf-8', 'ignore').replace(' ', ' ').replace('\t', '').replace('\n', '').replace('XLMMacroDeobfuscator: pywin32 is not installed (only is required if you want to use MS Excel)', ''), encoding="utf-8") failed = False if out.__len__() < 30: - logger.error('{} olevba returned <30 chars - rc: {!r}, response: {!r}, error: {!r}'.format(lid,cmd_tmp.returncode, + logger.error('{} olevba returned <30 chars - rc: {!r}, response: {!r}, error: {!r}'.format(lid,cmd_tmp.returncode, out.decode('utf-8', 'ignore'), err.decode('utf-8', 'ignore'))) out = b'[ { "error": "Unhandled error - too short olevba response" } ]' failed = True diff --git a/mailcow/data/Dockerfiles/phpfpm/Dockerfile b/mailcow/data/Dockerfiles/phpfpm/Dockerfile index 4ba8349..b8a7a43 100644 --- a/mailcow/data/Dockerfiles/phpfpm/Dockerfile +++ b/mailcow/data/Dockerfiles/phpfpm/Dockerfile @@ -3,15 +3,15 @@ FROM php:8.2-fpm-alpine3.21 LABEL maintainer = "The Infrastructure Company GmbH " # renovate: datasource=github-tags depName=krakjoe/apcu versioning=semver-coerced extractVersion=^v(?.*)$ -ARG APCU_PECL_VERSION=5.1.24 +ARG APCU_PECL_VERSION=5.1.27 # renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?.*)$ -ARG IMAGICK_PECL_VERSION=3.7.0 +ARG IMAGICK_PECL_VERSION=3.8.0 # renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?.*)$ -ARG MAILPARSE_PECL_VERSION=3.1.8 +ARG MAILPARSE_PECL_VERSION=3.1.9 # renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?.*)$ -ARG MEMCACHED_PECL_VERSION=3.2.0 +ARG MEMCACHED_PECL_VERSION=3.3.0 # renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?.*)$ -ARG REDIS_PECL_VERSION=6.1.0 +ARG REDIS_PECL_VERSION=6.2.0 # renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?.*)$ ARG COMPOSER_VERSION=2.8.6 diff --git a/mailcow/data/Dockerfiles/phpfpm/docker-entrypoint.sh b/mailcow/data/Dockerfiles/phpfpm/docker-entrypoint.sh index 0d09ac5..d7fa155 100755 --- a/mailcow/data/Dockerfiles/phpfpm/docker-entrypoint.sh +++ b/mailcow/data/Dockerfiles/phpfpm/docker-entrypoint.sh @@ -167,7 +167,7 @@ DELIMITER // CREATE EVENT clean_spamalias ON SCHEDULE EVERY 1 DAY DO BEGIN - DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP(); + DELETE FROM spamalias WHERE validity < UNIX_TIMESTAMP() AND permanent = 0; END; // DELIMITER ; diff --git a/mailcow/data/Dockerfiles/postfix-tlspol/Dockerfile b/mailcow/data/Dockerfiles/postfix-tlspol/Dockerfile new file mode 100644 index 0000000..68f6ecc --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix-tlspol/Dockerfile @@ -0,0 +1,50 @@ +FROM golang:1.25-bookworm AS builder +WORKDIR /src + +ENV CGO_ENABLED=0 \ + GO111MODULE=on \ + NOOPT=1 \ + VERSION=1.8.22 + +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 +LABEL maintainer="The Infrastructure Company GmbH " + +ARG DEBIAN_FRONTEND=noninteractive +ENV LC_ALL=C + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + dirmngr \ + dnsutils \ + iputils-ping \ + sudo \ + supervisor \ + redis-tools \ + syslog-ng \ + syslog-ng-core \ + syslog-ng-mod-redis \ + tzdata \ + && rm -rf /var/lib/apt/lists/* \ + && touch /etc/default/locale + +COPY supervisord.conf /etc/supervisor/supervisord.conf +COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf +COPY syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng-redis_slave.conf +COPY postfix-tlspol.sh /opt/postfix-tlspol.sh +COPY stop-supervisor.sh /usr/local/sbin/stop-supervisor.sh +COPY docker-entrypoint.sh /docker-entrypoint.sh +COPY --from=builder /src/postfix-tlspol/build/postfix-tlspol /usr/local/bin/postfix-tlspol + +RUN chmod +x /opt/postfix-tlspol.sh \ + /usr/local/sbin/stop-supervisor.sh \ + /docker-entrypoint.sh +RUN rm -rf /tmp/* /var/tmp/* + +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/postfix-tlspol/docker-entrypoint.sh b/mailcow/data/Dockerfiles/postfix-tlspol/docker-entrypoint.sh new file mode 100755 index 0000000..8c4f2c4 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix-tlspol/docker-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + cp /etc/syslog-ng/syslog-ng-redis_slave.conf /etc/syslog-ng/syslog-ng.conf +fi + +exec "$@" \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/postfix-tlspol/postfix-tlspol.sh b/mailcow/data/Dockerfiles/postfix-tlspol/postfix-tlspol.sh new file mode 100755 index 0000000..407a08f --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix-tlspol/postfix-tlspol.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +LOGLVL=info + +if [ ${DEV_MODE} != "n" ]; then + echo -e "\e[31mEnabling debug mode\e[0m" + set -x + LOGLVL=debug +fi + +[[ ! -d /etc/postfix-tlspol ]] && mkdir -p /etc/postfix-tlspol +[[ ! -d /var/lib/postfix-tlspol ]] && mkdir -p /var/lib/postfix-tlspol + +until dig +short mailcow.email > /dev/null; do + echo "Waiting for DNS..." + sleep 1 +done + +# Do not attempt to write to slave +if [[ ! -z ${REDIS_SLAVEOF_IP} ]]; then + export REDIS_CMDLINE="redis-cli -h ${REDIS_SLAVEOF_IP} -p ${REDIS_SLAVEOF_PORT} -a ${REDISPASS} --no-auth-warning" +else + export REDIS_CMDLINE="redis-cli -h redis -p 6379 -a ${REDISPASS} --no-auth-warning" +fi + +until [[ $(${REDIS_CMDLINE} PING) == "PONG" ]]; do + echo "Waiting for Redis..." + sleep 2 +done + +echo "Waiting for Postfix..." +until ping postfix -c1 > /dev/null; do + sleep 1 +done +echo "Postfix OK" + +cat < /etc/postfix-tlspol/config.yaml +server: + address: 0.0.0.0:8642 + + log-level: ${LOGLVL} + + prefetch: true + + cache-file: /var/lib/postfix-tlspol/cache.db + +dns: + # must support DNSSEC + address: 127.0.0.11:53 +EOF + +/usr/local/bin/postfix-tlspol -config /etc/postfix-tlspol/config.yaml \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/postfix-tlspol/stop-supervisor.sh b/mailcow/data/Dockerfiles/postfix-tlspol/stop-supervisor.sh new file mode 100755 index 0000000..5394490 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix-tlspol/stop-supervisor.sh @@ -0,0 +1,8 @@ +#!/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/mailcow/data/Dockerfiles/postfix-tlspol/supervisord.conf b/mailcow/data/Dockerfiles/postfix-tlspol/supervisord.conf new file mode 100644 index 0000000..90cf785 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix-tlspol/supervisord.conf @@ -0,0 +1,25 @@ +[supervisord] +pidfile=/var/run/supervisord.pid +nodaemon=true +user=root + +[program:syslog-ng] +command=/usr/sbin/syslog-ng --foreground --no-caps +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autostart=true + +[program:postfix-tlspol] +startsecs=10 +autorestart=true +command=/opt/postfix-tlspol.sh +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 + +[eventlistener:processes] +command=/usr/local/sbin/stop-supervisor.sh +events=PROCESS_STATE_STOPPED, PROCESS_STATE_EXITED, PROCESS_STATE_FATAL \ No newline at end of file diff --git a/mailcow/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf b/mailcow/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf new file mode 100644 index 0000000..3862a35 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix-tlspol/syslog-ng-redis_slave.conf @@ -0,0 +1,45 @@ +@version: 3.38 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + dns_cache(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats_freq(0); + bad_hostname("^gconfd$"); +}; +source s_src { + unix-stream("/dev/log"); + internal(); +}; +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { + redis( + host("`REDIS_SLAVEOF_IP`") + persist-name("redis1") + port(`REDIS_SLAVEOF_PORT`) + auth("`REDISPASS`") + command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") + ); +}; +filter f_mail { facility(mail); }; +# start +# overriding warnings are still displayed when the entrypoint runs its initial check +# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs +# Some other warnings are ignored +filter f_ignore { + not match("overriding earlier entry" value("MESSAGE")); + not match("TLS SNI from checks.mailcow.email" value("MESSAGE")); + not match("no SASL support" value("MESSAGE")); + not facility (local0, local1, local2, local3, local4, local5, local6, local7); +}; +# end +log { + source(s_src); + filter(f_ignore); + destination(d_stdout); + filter(f_mail); + destination(d_redis_ui_log); +}; diff --git a/mailcow/data/Dockerfiles/postfix-tlspol/syslog-ng.conf b/mailcow/data/Dockerfiles/postfix-tlspol/syslog-ng.conf new file mode 100644 index 0000000..7126c12 --- /dev/null +++ b/mailcow/data/Dockerfiles/postfix-tlspol/syslog-ng.conf @@ -0,0 +1,45 @@ +@version: 3.38 +@include "scl.conf" +options { + chain_hostnames(off); + flush_lines(0); + use_dns(no); + dns_cache(no); + use_fqdn(no); + owner("root"); group("adm"); perm(0640); + stats_freq(0); + bad_hostname("^gconfd$"); +}; +source s_src { + unix-stream("/dev/log"); + internal(); +}; +destination d_stdout { pipe("/dev/stdout"); }; +destination d_redis_ui_log { + redis( + host("redis-mailcow") + persist-name("redis1") + port(6379) + auth("`REDISPASS`") + command("LPUSH" "POSTFIX_MAILLOG" "$(format-json time=\"$S_UNIXTIME\" priority=\"$PRIORITY\" program=\"$PROGRAM\" message=\"$MESSAGE\")\n") + ); +}; +filter f_mail { facility(mail); }; +# start +# overriding warnings are still displayed when the entrypoint runs its initial check +# warnings logged by postfix-mailcow to syslog are hidden to reduce repeating msgs +# Some other warnings are ignored +filter f_ignore { + not match("overriding earlier entry" value("MESSAGE")); + not match("TLS SNI from checks.mailcow.email" value("MESSAGE")); + not match("no SASL support" value("MESSAGE")); + not facility (local0, local1, local2, local3, local4, local5, local6, local7); +}; +# end +log { + source(s_src); + filter(f_ignore); + destination(d_stdout); + filter(f_mail); + destination(d_redis_ui_log); +}; diff --git a/mailcow/data/Dockerfiles/postfix/Dockerfile b/mailcow/data/Dockerfiles/postfix/Dockerfile index 5449360..994612e 100644 --- a/mailcow/data/Dockerfiles/postfix/Dockerfile +++ b/mailcow/data/Dockerfiles/postfix/Dockerfile @@ -1,9 +1,9 @@ FROM debian:bookworm-slim -LABEL maintainer = "The Infrastructure Company GmbH " +LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ENV LC_ALL C +ENV LC_ALL=C RUN dpkg-divert --local --rename --add /sbin/initctl \ && ln -sf /bin/true /sbin/initctl \ diff --git a/mailcow/data/Dockerfiles/postfix/postfix.sh b/mailcow/data/Dockerfiles/postfix/postfix.sh index e5dbf88..0a8ed73 100755 --- a/mailcow/data/Dockerfiles/postfix/postfix.sh +++ b/mailcow/data/Dockerfiles/postfix/postfix.sh @@ -390,7 +390,7 @@ hosts = unix:/var/run/mysqld/mysqld.sock dbname = ${DBNAME} query = SELECT goto FROM spamalias WHERE address='%s' - AND validity >= UNIX_TIMESTAMP() + AND (validity >= UNIX_TIMESTAMP() OR permanent != 0) EOF if [ ! -f /opt/postfix/conf/dns_blocklists.cf ]; then diff --git a/mailcow/data/Dockerfiles/rspamd/Dockerfile b/mailcow/data/Dockerfiles/rspamd/Dockerfile index 68b38d3..061f3d1 100644 --- a/mailcow/data/Dockerfiles/rspamd/Dockerfile +++ b/mailcow/data/Dockerfiles/rspamd/Dockerfile @@ -2,7 +2,7 @@ FROM debian:bookworm-slim LABEL maintainer="The Infrastructure Company GmbH " ARG DEBIAN_FRONTEND=noninteractive -ARG RSPAMD_VER=rspamd_3.11.1-1~ab0b44951 +ARG RSPAMD_VER=rspamd_3.13.2-1~8bf602278 ARG CODENAME=bookworm ENV LC_ALL=C @@ -14,8 +14,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ dnsutils \ netcat-traditional \ wget \ - redis-tools \ - procps \ + redis-tools \ + procps \ nano \ lua-cjson \ && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \ diff --git a/mailcow/data/Dockerfiles/rspamd/docker-entrypoint.sh b/mailcow/data/Dockerfiles/rspamd/docker-entrypoint.sh index cf44c30..a764725 100755 --- a/mailcow/data/Dockerfiles/rspamd/docker-entrypoint.sh +++ b/mailcow/data/Dockerfiles/rspamd/docker-entrypoint.sh @@ -81,6 +81,29 @@ EOF redis-cli -h redis-mailcow -a ${REDISPASS} --no-auth-warning SLAVEOF NO ONE fi +if [[ "${SKIP_OLEFY}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + if [[ -f /etc/rspamd/local.d/external_services.conf ]]; then + rm /etc/rspamd/local.d/external_services.conf + fi +else + if [[ ! -f /etc/rspamd/local.d/external_services.conf ]]; then + cat < /etc/rspamd/local.d/external_services.conf +oletools { + # default olefy settings + servers = "olefy:10055"; + # needs to be set explicitly for Rspamd < 1.9.5 + scan_mime_parts = true; + # mime-part regex matching in content-type or filename + # block all macros + extended = true; + max_size = 3145728; + timeout = 20.0; + retransmits = 1; +} +EOF + fi +fi + # Provide additional lua modules ln -s /usr/lib/$(uname -m)-linux-gnu/liblua5.1-cjson.so.0.0.0 /usr/lib/rspamd/cjson.so diff --git a/mailcow/data/Dockerfiles/sogo/bootstrap-sogo.sh b/mailcow/data/Dockerfiles/sogo/bootstrap-sogo.sh index abd398b..96d8a69 100755 --- a/mailcow/data/Dockerfiles/sogo/bootstrap-sogo.sh +++ b/mailcow/data/Dockerfiles/sogo/bootstrap-sogo.sh @@ -24,6 +24,10 @@ while [[ "${DBV_NOW}" != "${DBV_NEW}" ]]; do done echo "DB schema is ${DBV_NOW}" +if [[ "${MASTER}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + mariadb --skip-ssl --socket=/var/run/mysqld/mysqld.sock -u ${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TRIGGER IF EXISTS sogo_update_password" +fi + # cat /dev/urandom seems to hang here occasionally and is not recommended anyway, better use openssl RAND_PASS=$(openssl rand -base64 16 | tr -dc _A-Z-a-z-0-9) diff --git a/mailcow/data/Dockerfiles/watchdog/Dockerfile b/mailcow/data/Dockerfiles/watchdog/Dockerfile index a55a97a..6d8541d 100644 --- a/mailcow/data/Dockerfiles/watchdog/Dockerfile +++ b/mailcow/data/Dockerfiles/watchdog/Dockerfile @@ -16,7 +16,6 @@ RUN apk add --update \ fcgi \ openssl \ nagios-plugins-mysql \ - nagios-plugins-dns \ nagios-plugins-disk \ bind-tools \ redis \ @@ -32,9 +31,11 @@ RUN apk add --update \ tzdata \ whois \ && curl https://raw.githubusercontent.com/mludvig/smtp-cli/v3.10/smtp-cli -o /smtp-cli \ - && chmod +x smtp-cli + && chmod +x smtp-cli \ + && mkdir /usr/lib/mailcow 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 CMD ["/watchdog.sh"] diff --git a/mailcow/data/Dockerfiles/watchdog/check_dns.sh b/mailcow/data/Dockerfiles/watchdog/check_dns.sh new file mode 100755 index 0000000..ce4cfa3 --- /dev/null +++ b/mailcow/data/Dockerfiles/watchdog/check_dns.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +while getopts "H:s:" opt; do + case "$opt" in + H) HOST="$OPTARG" ;; + s) SERVER="$OPTARG" ;; + *) echo "Usage: $0 -H host -s server"; exit 3 ;; + esac +done + +if [ -z "$SERVER" ]; then + echo "No DNS Server provided" + exit 3 +fi + +if [ -z "$HOST" ]; then + echo "No host to test provided" + exit 3 +fi + +# run dig and measure the time it takes to run +START_TIME=$(date +%s%3N) +dig_output=$(dig +short +timeout=2 +tries=1 "$HOST" @"$SERVER" 2>/dev/null) +dig_rc=$? +dig_output_ips=$(echo "$dig_output" | grep -E '^[0-9.]+$' | sort | paste -sd ',' -) +END_TIME=$(date +%s%3N) +ELAPSED_TIME=$((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" + exit 0 +else + echo "Unknown error" + exit 3 +fi diff --git a/mailcow/data/Dockerfiles/watchdog/watchdog.sh b/mailcow/data/Dockerfiles/watchdog/watchdog.sh index 9d6f660..558aaa1 100755 --- a/mailcow/data/Dockerfiles/watchdog/watchdog.sh +++ b/mailcow/data/Dockerfiles/watchdog/watchdog.sh @@ -1,5 +1,10 @@ #!/bin/bash +if [ "${DEV_MODE}" != "n" ]; then + echo -e "\e[31mEnabled Debug Mode\e[0m" + set -x +fi + trap "exit" INT TERM trap "kill 0" EXIT @@ -297,7 +302,7 @@ unbound_checks() { touch /tmp/unbound-mailcow; echo "$(tail -50 /tmp/unbound-mailcow)" > /tmp/unbound-mailcow host_ip=$(get_container_ip unbound-mailcow) err_c_cur=${err_count} - /usr/lib/nagios/plugins/check_dns -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? )) + /usr/lib/mailcow/check_dns.sh -s ${host_ip} -H stackoverflow.com 2>> /tmp/unbound-mailcow 1>&2; err_count=$(( ${err_count} + $? )) DNSSEC=$(dig com +dnssec | egrep 'flags:.+ad') if [[ -z ${DNSSEC} ]]; then echo "DNSSEC failure" 2>> /tmp/unbound-mailcow 1>&2 @@ -445,6 +450,31 @@ postfix_checks() { return 1 } +postfix-tlspol_checks() { + err_count=0 + diff_c=0 + THRESHOLD=${POSTFIX_TLSPOL_THRESHOLD} + # Reduce error count by 2 after restarting an unhealthy container + trap "[ ${err_count} -gt 1 ] && err_count=$(( ${err_count} - 2 ))" USR1 + while [ ${err_count} -lt ${THRESHOLD} ]; do + touch /tmp/postfix-tlspol-mailcow; echo "$(tail -50 /tmp/postfix-tlspol-mailcow)" > /tmp/postfix-tlspol-mailcow + host_ip=$(get_container_ip postfix-tlspol-mailcow) + err_c_cur=${err_count} + /usr/lib/nagios/plugins/check_tcp -4 -H ${host_ip} -p 8642 2>> /tmp/postfix-tlspol-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 "Postfix TLS Policy companion" ${THRESHOLD} $(( ${THRESHOLD} - ${err_count} )) ${diff_c} + if [[ $? == 10 ]]; then + diff_c=0 + sleep 1 + else + diff_c=0 + sleep $(( ( RANDOM % 60 ) + 20 )) + fi + done + return 1 +} + clamd_checks() { err_count=0 diff_c=0 @@ -922,6 +952,18 @@ PID=$! echo "Spawned mailq_checks with PID ${PID}" BACKGROUND_TASKS+=(${PID}) +( +while true; do + if ! postfix-tlspol_checks; then + log_msg "Postfix TLS Policy hit error limit" + echo postfix-tlspol-mailcow > /tmp/com_pipe + fi +done +) & +PID=$! +echo "Spawned postfix-tlspol_checks with PID ${PID}" +BACKGROUND_TASKS+=(${PID}) + ( while true; do if ! dovecot_checks; then @@ -994,6 +1036,7 @@ PID=$! echo "Spawned cert_checks with PID ${PID}" BACKGROUND_TASKS+=(${PID}) +if [[ "${SKIP_OLEFY}" =~ ^([nN][oO]|[nN])+$ ]]; then ( while true; do if ! olefy_checks; then @@ -1005,6 +1048,7 @@ done PID=$! echo "Spawned olefy_checks with PID ${PID}" BACKGROUND_TASKS+=(${PID}) +fi ( while true; do diff --git a/mailcow/data/conf/dovecot/auth/mailcowauth.php b/mailcow/data/conf/dovecot/auth/mailcowauth.php index 667419c..06c0bd9 100644 --- a/mailcow/data/conf/dovecot/auth/mailcowauth.php +++ b/mailcow/data/conf/dovecot/auth/mailcowauth.php @@ -79,12 +79,14 @@ if ($isSOGoRequest) { } } if ($result === false){ - $result = apppass_login($post['username'], $post['password'], array($post['service'] => true), array( + // If it's a SOGo Request, don't check for protocol access + $service = ($isSOGoRequest) ? false : array($post['service'] => true); + $result = apppass_login($post['username'], $post['password'], $service, array( 'is_internal' => true, 'remote_addr' => $post['real_rip'] )); if ($result) { - error_log('MAILCOWAUTH: App auth for user ' . $post['username']); + error_log('MAILCOWAUTH: App auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']); set_sasl_log($post['username'], $post['real_rip'], $post['service']); } } @@ -92,9 +94,9 @@ if ($result === false){ // Init Identity Provider $iam_provider = identity_provider('init'); $iam_settings = identity_provider('get'); - $result = user_login($post['username'], $post['password'], array('is_internal' => true)); + $result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service'])); if ($result) { - error_log('MAILCOWAUTH: User auth for user ' . $post['username']); + error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']); set_sasl_log($post['username'], $post['real_rip'], $post['service']); } } @@ -103,7 +105,7 @@ if ($result) { http_response_code(200); // OK $return['success'] = true; } else { - error_log("MAILCOWAUTH: Login failed for user " . $post['username']); + error_log("MAILCOWAUTH: Login failed for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']); http_response_code(401); // Unauthorized } diff --git a/mailcow/data/conf/dovecot/auth/passwd-verify.lua b/mailcow/data/conf/dovecot/auth/passwd-verify.lua deleted file mode 100644 index 18c18db..0000000 --- a/mailcow/data/conf/dovecot/auth/passwd-verify.lua +++ /dev/null @@ -1,47 +0,0 @@ -function auth_password_verify(request, password) - if request.domain == nil then - return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "No such user" - end - - local json = require "cjson" - local ltn12 = require "ltn12" - local https = require "ssl.https" - https.TIMEOUT = 30 - - local req = { - username = request.user, - password = password, - real_rip = request.real_rip, - service = request.service - } - local req_json = json.encode(req) - local res = {} - - local b, c = https.request { - method = "POST", - url = "https://nginx:9082", - source = ltn12.source.string(req_json), - headers = { - ["content-type"] = "application/json", - ["content-length"] = tostring(#req_json) - }, - sink = ltn12.sink.table(res), - insecure = true - } - - if c ~= 200 then - dovecot.i_info("HTTP request failed with " .. c .. " for user " .. request.user) - return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream error" - end - - local api_response = json.decode(table.concat(res)) - if api_response.success == true then - return dovecot.auth.PASSDB_RESULT_OK, "" - end - - return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Failed to authenticate" -end - -function auth_passdb_lookup(req) - return dovecot.auth.PASSDB_RESULT_USER_UNKNOWN, "" -end diff --git a/mailcow/data/conf/nginx/templates/nginx.conf.j2 b/mailcow/data/conf/nginx/templates/nginx.conf.j2 index ff5f8f1..08a85a1 100644 --- a/mailcow/data/conf/nginx/templates/nginx.conf.j2 +++ b/mailcow/data/conf/nginx/templates/nginx.conf.j2 @@ -13,6 +13,7 @@ events { http { include /etc/nginx/mime.types; default_type application/octet-stream; + server_tokens off; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' @@ -48,13 +49,21 @@ http { listen {{ HTTP_PORT }} default_server; listen [::]:{{ HTTP_PORT }} default_server; - server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }}; + server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.* {{ ADDITIONAL_SERVER_NAMES | join(' ') }}; if ( $request_uri ~* "%0A|%0D" ) { return 403; } location ^~ /.well-known/acme-challenge/ { allow all; default_type "text/plain"; } + location ^~ /.well-known/mta-sts.txt { + allow all; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php; + fastcgi_param PATH_INFO $fastcgi_path_info; + } location / { return 301 https://$host$uri$is_args$args; } @@ -70,7 +79,7 @@ http { {%endif%} listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; - {% if not DISABLE_IPv6 %} + {% if ENABLE_IPV6 %} {% if not HTTP_REDIRECT %} listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; {%endif%} @@ -82,7 +91,7 @@ http { ssl_certificate /etc/ssl/mail/cert.pem; ssl_certificate_key /etc/ssl/mail/key.pem; - server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.*; + server_name {{ MAILCOW_HOSTNAME }} autodiscover.* autoconfig.* mta-sts.*; include /etc/nginx/includes/sites-default.conf; } @@ -97,7 +106,7 @@ http { {%endif%} listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; - {% if not DISABLE_IPv6 %} + {% if ENABLE_IPV6 %} {% if not HTTP_REDIRECT %} listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; {%endif%} @@ -118,7 +127,7 @@ http { # rspamd dynmaps: server { listen 8081; - {% if not DISABLE_IPv6 %} + {% if ENABLE_IPV6 %} listen [::]:8081; {%endif%} index index.php index.html; @@ -191,7 +200,7 @@ http { {%endif%} listen {{ HTTPS_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%} ssl; - {% if not DISABLE_IPv6 %} + {% if ENABLE_IPV6 %} {% if not HTTP_REDIRECT %} listen [::]:{{ HTTP_PORT }}{% if NGINX_USE_PROXY_PROTOCOL %} proxy_protocol{%endif%}; {%endif%} diff --git a/mailcow/data/conf/nginx/templates/sites-default.conf.j2 b/mailcow/data/conf/nginx/templates/sites-default.conf.j2 index 574bdb0..f3d7345 100644 --- a/mailcow/data/conf/nginx/templates/sites-default.conf.j2 +++ b/mailcow/data/conf/nginx/templates/sites-default.conf.j2 @@ -14,7 +14,6 @@ ssl_session_tickets off; add_header Strict-Transport-Security "max-age=15768000;"; add_header X-Content-Type-Options nosniff; -add_header X-XSS-Protection "1; mode=block"; add_header X-Robots-Tag none; add_header X-Download-Options noopen; add_header X-Frame-Options "SAMEORIGIN" always; @@ -76,6 +75,14 @@ location ^~ /.well-known/acme-challenge/ { allow all; default_type "text/plain"; } +location ^~ /.well-known/mta-sts.txt { + allow all; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass {{ PHPFPMHOST }}:9002; + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root/mta-sts.php; + fastcgi_param PATH_INFO $fastcgi_path_info; +} rewrite ^/.well-known/caldav$ /SOGo/dav/ permanent; rewrite ^/.well-known/carddav$ /SOGo/dav/ permanent; diff --git a/mailcow/data/conf/phpfpm/php-conf.d/opcache-recommended.ini b/mailcow/data/conf/phpfpm/php-conf.d/opcache-recommended.ini index 3c51e11..452b454 100644 --- a/mailcow/data/conf/phpfpm/php-conf.d/opcache-recommended.ini +++ b/mailcow/data/conf/phpfpm/php-conf.d/opcache-recommended.ini @@ -1,7 +1,16 @@ +; NOTE: Restart phpfpm on ANY manual changes to PHP files! + +; opcache opcache.enable=1 opcache.enable_cli=1 opcache.interned_strings_buffer=16 opcache.max_accelerated_files=10000 opcache.memory_consumption=128 opcache.save_comments=1 -opcache.revalidate_freq=1 +opcache.validate_timestamps=0 + +; JIT +; Disabled for now due to some PHP segmentation faults observed +; in certain environments. Possibly some PHP or PHP extension bug. +opcache.jit=disable +opcache.jit_buffer_size=0 diff --git a/mailcow/data/conf/postfix/main.cf b/mailcow/data/conf/postfix/main.cf index 9717bfb..810999c 100644 --- a/mailcow/data/conf/postfix/main.cf +++ b/mailcow/data/conf/postfix/main.cf @@ -152,7 +152,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 +smtp_tls_policy_maps = proxy:mysql:/opt/postfix/conf/sql/mysql_tls_policy_override_maps.cf socketmap:inet:postfix-tlspol:8642:QUERY 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 "*" @@ -192,7 +192,10 @@ postscreen_dnsbl_sites = wl.mailspike.net=127.0.0.[18;19;20]*-2 b.barracudacentral.org=127.0.0.2*7 bl.mailspike.net=127.0.0.2*5 bl.mailspike.net=127.0.0.[10;11;12]*4 - + zen.spamhaus.org=127.0.0.[10;11]*8 + zen.spamhaus.org=127.0.0.[4..7]*6 + zen.spamhaus.org=127.0.0.3*4 + zen.spamhaus.org=127.0.0.2*3 # User Overrides myhostname = mail.nasarek.dev diff --git a/mailcow/data/conf/postfix/postscreen_access.cidr b/mailcow/data/conf/postfix/postscreen_access.cidr index 36c7e9f..694b986 100644 --- a/mailcow/data/conf/postfix/postscreen_access.cidr +++ b/mailcow/data/conf/postfix/postscreen_access.cidr @@ -1,13 +1,26 @@ -# Whitelist generated by Postwhite v3.4 on Tue Apr 1 00:20:51 UTC 2025 +# Whitelist generated by Postwhite v3.4 on Mon Dec 1 00:24:43 UTC 2025 # https://github.com/stevejenkins/postwhite/ -# 2067 total rules +# 2186 total rules 2a00:1450:4000::/36 permit 2a01:111:f400::/48 permit -2a01:111:f403:8000::/50 permit +2a01:111:f403:2800::/53 permit 2a01:111:f403:8000::/51 permit 2a01:111:f403::/49 permit 2a01:111:f403:c000::/51 permit +2a01:111:f403:d000::/53 permit 2a01:111:f403:f000::/52 permit +2a01:238:20a:202:5370::1 permit +2a01:238:20a:202:5372::1 permit +2a01:238:20a:202:5373::1 permit +2a01:238:400:101:53::1 permit +2a01:238:400:102:53::1 permit +2a01:238:400:103:53::1 permit +2a01:238:400:301:53::1 permit +2a01:238:400:302:53::1 permit +2a01:238:400:303:53::1 permit +2a01:238:400:470:53::1 permit +2a01:238:400:471:53::1 permit +2a01:238:400:472:53::1 permit 2a01:b747:3000:200::/56 permit 2a01:b747:3001:200::/56 permit 2a01:b747:3002:200::/56 permit @@ -16,28 +29,43 @@ 2a01:b747:3005:200::/56 permit 2a01:b747:3006:200::/56 permit 2a02:a60:0:5::/64 permit +2a0f:f640::/56 permit 2c0f:fb50:4000::/36 permit 2.207.151.53 permit +2.207.217.30 permit +3.64.237.68 permit +3.65.3.180 permit 3.70.123.177 permit +3.72.182.33 permit +3.74.81.189 permit +3.74.125.228 permit +3.75.33.185 permit 3.93.157.0/24 permit 3.94.40.108 permit +3.121.107.214 permit 3.129.120.190 permit 3.210.190.0/24 permit +3.211.80.218 permit +3.216.221.67 permit +3.221.209.22 permit 8.20.114.31 permit 8.25.194.0/23 permit 8.25.196.0/23 permit -8.39.54.0/23 permit -8.39.54.250/31 permit -8.40.222.0/23 permit -8.40.222.250/31 permit +8.36.116.0/24 permit +8.39.144.0/24 permit 12.130.86.238 permit -13.107.246.59 permit +13.107.213.69 permit +13.107.246.69 permit +13.108.16.0/20 permit 13.110.208.0/21 permit 13.110.209.0/24 permit 13.110.216.0/22 permit 13.110.224.0/20 permit 13.111.0.0/16 permit 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 @@ -50,16 +78,21 @@ 18.97.1.184/29 permit 18.97.2.64/26 permit 18.156.89.250 permit +18.156.205.64 permit +18.157.70.148 permit +18.157.114.255 permit 18.157.243.190 permit +18.158.153.154 permit 18.194.95.56 permit +18.197.217.180 permit 18.198.96.88 permit +18.199.210.3 permit +18.207.52.234 permit 18.208.124.128/25 permit 18.216.232.154 permit 18.235.27.253 permit 18.236.40.242 permit -18.236.56.161 permit 20.51.6.32/30 permit -20.51.98.61 permit 20.52.52.2 permit 20.52.128.133 permit 20.59.80.4/30 permit @@ -89,6 +122,7 @@ 23.253.183.147 permit 23.253.183.148 permit 23.253.183.150 permit +24.110.64.0/18 permit 27.123.204.128/30 permit 27.123.204.132/31 permit 27.123.204.148/30 permit @@ -101,8 +135,8 @@ 27.123.206.56/29 permit 27.123.206.76/30 permit 27.123.206.80/28 permit -31.25.48.222 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 @@ -121,17 +155,30 @@ 34.2.90.0/23 permit 34.2.92.0/23 permit 34.2.94.0/23 permit +34.70.158.162 permit +34.74.74.140 permit +34.83.159.189 permit +34.141.160.224 permit +34.193.58.168 permit 34.195.217.107 permit +34.197.10.50 permit +34.197.254.9 permit +34.198.94.229 permit +34.198.218.121 permit 34.212.163.75 permit 34.215.104.144 permit -34.218.116.3 permit +34.218.115.239 permit 34.225.212.172 permit +35.83.148.184 permit +35.155.198.111 permit +35.158.23.94 permit 35.161.32.253 permit +35.162.73.231 permit 35.167.93.243 permit +35.174.145.124 permit 35.176.132.251 permit -35.190.247.0/24 permit -35.191.0.0/16 permit 35.205.92.9 permit +35.228.216.85 permit 35.242.169.159 permit 37.188.97.188 permit 37.218.248.47 permit @@ -146,11 +193,19 @@ 40.233.83.78 permit 40.233.88.28 permit 44.206.138.57 permit +44.210.169.44 permit 44.217.45.156 permit 44.236.56.93 permit 44.238.220.251 permit +44.245.243.92 permit +44.246.1.125 permit +44.246.68.102 permit +44.246.77.92 permit 45.14.148.0/22 permit -46.19.170.16 permit +45.143.132.0/24 permit +45.143.133.0/24 permit +45.143.134.0/24 permit +45.143.135.0/24 permit 46.226.48.0/21 permit 46.228.36.37 permit 46.228.36.38/31 permit @@ -201,6 +256,7 @@ 46.243.88.177 permit 46.243.95.179 permit 46.243.95.180 permit +50.16.246.183 permit 50.18.45.249 permit 50.18.121.236 permit 50.18.121.248 permit @@ -214,14 +270,23 @@ 50.56.130.220 permit 50.56.130.221 permit 50.56.130.222 permit +50.112.246.219 permit 52.1.14.157 permit 52.5.230.59 permit +52.12.53.23 permit +52.13.214.179 permit +52.26.1.71 permit 52.27.5.72 permit 52.27.28.47 permit 52.28.63.81 permit +52.28.197.132 permit +52.34.181.151 permit +52.35.192.45 permit 52.36.138.31 permit 52.37.142.146 permit +52.42.203.116 permit 52.50.24.208 permit +52.57.120.243 permit 52.58.216.183 permit 52.59.143.3 permit 52.60.41.5 permit @@ -233,7 +298,7 @@ 52.95.49.88/29 permit 52.96.91.34 permit 52.96.111.82 permit -52.96.214.50 permit +52.96.172.98 permit 52.96.222.194 permit 52.96.222.226 permit 52.96.223.2 permit @@ -263,24 +328,23 @@ 54.174.63.0/24 permit 54.186.193.102 permit 54.191.223.56 permit +54.211.126.101 permit 54.213.20.246 permit 54.214.39.184 permit 54.240.0.0/18 permit -54.240.64.0/19 permit -54.240.96.0/19 permit +54.240.64.0/18 permit 54.241.16.209 permit 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 62.13.129.128/25 permit 62.13.136.0/21 permit 62.13.144.0/21 permit 62.13.152.0/21 permit 62.17.146.128/26 permit -62.179.121.0/24 permit 62.201.172.0/27 permit 62.201.172.32/27 permit 62.253.227.114 permit @@ -288,6 +352,9 @@ 63.128.21.0/24 permit 63.143.57.128/25 permit 63.143.59.128/25 permit +63.176.194.123 permit +63.178.132.221 permit +63.178.143.178 permit 64.18.0.0/20 permit 64.20.241.45 permit 64.69.212.0/24 permit @@ -300,6 +367,7 @@ 64.127.115.252 permit 64.132.88.0/23 permit 64.132.92.0/24 permit +64.181.194.190 permit 64.207.219.7 permit 64.207.219.8 permit 64.207.219.9 permit @@ -309,9 +377,6 @@ 64.207.219.13 permit 64.207.219.14 permit 64.207.219.15 permit -64.207.219.24 permit -64.207.219.25 permit -64.207.219.26 permit 64.207.219.71 permit 64.207.219.72 permit 64.207.219.73 permit @@ -321,9 +386,6 @@ 64.207.219.77 permit 64.207.219.78 permit 64.207.219.79 permit -64.207.219.88 permit -64.207.219.89 permit -64.207.219.90 permit 64.207.219.135 permit 64.207.219.136 permit 64.207.219.137 permit @@ -335,23 +397,15 @@ 64.207.219.143 permit 64.233.160.0/19 permit 65.52.80.137 permit -65.54.51.64/26 permit -65.54.61.64/26 permit 65.54.121.120/29 permit -65.54.190.0/24 permit -65.54.241.0/24 permit 65.55.29.77 permit 65.55.33.64/28 permit -65.55.34.0/24 permit 65.55.42.224/28 permit 65.55.52.224/27 permit 65.55.78.128/25 permit 65.55.81.48/28 permit -65.55.90.0/24 permit 65.55.94.0/25 permit -65.55.111.0/24 permit 65.55.113.64/26 permit -65.55.116.0/25 permit 65.55.126.0/25 permit 65.55.174.0/25 permit 65.55.178.128/27 permit @@ -359,11 +413,9 @@ 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 -66.162.193.226/31 permit 66.163.184.0/24 permit 66.163.185.0/24 permit 66.163.186.0/24 permit @@ -569,13 +621,11 @@ 74.86.241.250/31 permit 74.112.67.243 permit 74.125.0.0/16 permit -74.202.227.40 permit 74.208.4.200 permit 74.208.4.201 permit 74.208.4.220 permit 74.208.4.221 permit 74.209.250.0/24 permit -75.2.70.75 permit 76.223.128.0/19 permit 76.223.176.0/20 permit 77.238.176.0/24 permit @@ -598,25 +648,31 @@ 77.238.189.142 permit 77.238.189.146/31 permit 77.238.189.148/30 permit +79.135.106.0/24 permit +79.135.107.0/24 permit +81.169.146.243 permit +81.169.146.245 permit +81.169.146.246 permit 81.223.46.0/27 permit -82.165.159.2 permit -82.165.159.3 permit -82.165.159.4 permit 82.165.159.12 permit 82.165.159.13 permit 82.165.159.14 permit -82.165.159.34 permit -82.165.159.35 permit 82.165.159.40 permit 82.165.159.41 permit 82.165.159.42 permit -82.165.159.45 permit 82.165.159.130 permit 82.165.159.131 permit -84.116.6.0/23 permit -84.116.36.0/24 permit -84.116.50.0/23 permit +85.9.206.169 permit +85.9.210.45 permit 85.158.136.0/21 permit +85.215.255.39 permit +85.215.255.40 permit +85.215.255.41 permit +85.215.255.45 permit +85.215.255.46 permit +85.215.255.47 permit +85.215.255.48 permit +85.215.255.49 permit 86.61.88.25 permit 87.238.80.0/21 permit 87.248.103.12 permit @@ -656,12 +712,13 @@ 87.248.117.205 permit 87.253.232.0/21 permit 89.22.108.0/24 permit +91.198.2.0/24 permit 91.211.240.0/22 permit -94.169.2.0/23 permit 94.236.119.0/26 permit 94.245.112.0/27 permit 94.245.112.10/31 permit 95.131.104.0/21 permit +95.217.114.154 permit 96.43.144.0/20 permit 96.43.144.64/28 permit 96.43.144.64/31 permit @@ -1152,7 +1209,6 @@ 98.139.245.208/30 permit 98.139.245.212/31 permit 99.78.197.208/28 permit -99.83.190.102 permit 103.9.96.0/22 permit 103.28.42.0/24 permit 103.151.192.0/23 permit @@ -1161,9 +1217,6 @@ 104.43.243.237 permit 104.44.112.128/25 permit 104.47.0.0/17 permit -104.47.20.0/23 permit -104.47.75.0/24 permit -104.47.108.0/23 permit 104.130.96.0/28 permit 104.130.122.0/23 permit 106.10.144.64/27 permit @@ -1289,6 +1342,7 @@ 106.50.16.0/28 permit 107.20.18.111 permit 107.20.210.250 permit +107.22.191.150 permit 108.174.0.0/24 permit 108.174.0.215 permit 108.174.3.0/24 permit @@ -1297,9 +1351,8 @@ 108.174.6.215 permit 108.175.18.45 permit 108.175.30.45 permit -108.177.8.0/21 permit -108.177.96.0/19 permit 108.179.144.0/20 permit +109.224.244.0/24 permit 109.237.142.0/24 permit 111.221.23.128/25 permit 111.221.26.0/27 permit @@ -1323,9 +1376,6 @@ 117.120.16.0/21 permit 119.42.242.52/31 permit 119.42.242.156 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 @@ -1353,6 +1403,7 @@ 128.245.248.0/21 permit 129.41.77.70 permit 129.41.169.249 permit +129.77.16.0/20 permit 129.80.5.164 permit 129.80.64.36 permit 129.80.67.121 permit @@ -1374,7 +1425,6 @@ 129.213.195.191 permit 130.61.9.72 permit 130.162.39.83 permit -130.211.0.0/22 permit 130.248.172.0/24 permit 130.248.173.0/24 permit 131.253.30.0/24 permit @@ -1383,15 +1433,13 @@ 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.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 @@ -1403,6 +1451,7 @@ 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 136.147.176.0/20 permit @@ -1417,7 +1466,6 @@ 139.138.46.219 permit 139.138.57.55 permit 139.138.58.119 permit -139.167.79.86 permit 139.180.17.0/24 permit 140.238.148.191 permit 141.148.159.229 permit @@ -1452,6 +1500,7 @@ 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.243.1.47 permit 147.243.1.48 permit @@ -1461,7 +1510,7 @@ 148.105.0.0/16 permit 148.105.8.0/21 permit 149.72.0.0/16 permit -149.72.223.204 permit +149.72.234.184 permit 149.72.248.236 permit 149.97.173.180 permit 150.230.98.160 permit @@ -1473,9 +1522,6 @@ 155.248.220.138 permit 155.248.234.149 permit 155.248.237.141 permit -157.55.0.192/26 permit -157.55.1.128/26 permit -157.55.2.0/25 permit 157.55.9.128/25 permit 157.55.11.0/25 permit 157.55.49.0/25 permit @@ -1517,6 +1563,7 @@ 159.183.0.0/16 permit 159.183.68.71 permit 159.183.79.38 permit +159.183.129.172 permit 160.1.62.192 permit 161.38.192.0/20 permit 161.38.204.0/22 permit @@ -1533,11 +1580,11 @@ 163.114.132.120 permit 163.114.134.16 permit 163.114.135.16 permit +163.116.128.0/17 permit +163.192.116.87 permit 164.152.23.32 permit +164.152.25.241 permit 164.177.132.168/30 permit -165.173.128.0/24 permit -165.173.180.250/31 permit -165.173.182.250/31 permit 166.78.68.0/22 permit 166.78.68.221 permit 166.78.69.169 permit @@ -1562,32 +1609,21 @@ 168.138.5.36 permit 168.138.73.51 permit 168.138.77.31 permit +168.138.237.153 permit 168.245.0.0/17 permit 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.142.10 permit -169.148.144.0/25 permit -169.148.144.10 permit -169.148.146.0/23 permit 170.10.128.0/24 permit 170.10.129.0/24 permit 170.10.132.56/29 permit 170.10.132.64/29 permit 170.10.133.0/24 permit -172.217.0.0/19 permit -172.217.32.0/20 permit -172.217.128.0/19 permit -172.217.160.0/20 permit -172.217.192.0/19 permit -172.253.56.0/21 permit -172.253.112.0/20 permit 173.0.84.0/29 permit 173.0.84.224/27 permit 173.0.94.244/30 permit 173.194.0.0/16 permit +173.194.0.0/17 permit 173.203.79.182 permit 173.203.81.39 permit 173.224.161.128/25 permit @@ -1611,9 +1647,14 @@ 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.12.80.0/22 permit 185.28.196.0/22 permit 185.58.84.93 permit +185.70.40.0/24 permit +185.70.41.0/24 permit +185.70.43.0/24 permit 185.80.93.204 permit 185.80.93.227 permit 185.80.95.31 permit @@ -1621,6 +1662,8 @@ 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.250.236.0/22 permit 185.250.239.148 permit 185.250.239.168 permit @@ -1673,6 +1716,7 @@ 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.18.139.154 permit @@ -1695,10 +1739,31 @@ 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.191 permit +193.142.157.198 permit 194.19.134.0/25 permit +194.25.134.16/28 permit +194.25.134.80/28 permit 194.64.234.129 permit +194.97.196.0/24 permit +194.97.196.3 permit +194.97.196.4 permit +194.97.196.11 permit +194.97.196.12 permit +194.97.204.0/24 permit +194.97.204.3 permit +194.97.204.4 permit +194.97.204.11 permit +194.97.204.12 permit +194.97.212.0/24 permit +194.97.212.3 permit +194.97.212.4 permit +194.97.212.11 permit +194.97.212.12 permit 194.106.220.0/23 permit 194.113.24.0/22 permit +194.113.42.0/26 permit 194.154.193.192/27 permit 195.4.92.0/23 permit 195.54.172.0/23 permit @@ -1712,6 +1777,7 @@ 198.61.254.21 permit 198.61.254.231 permit 198.178.234.57 permit +198.202.211.1 permit 198.244.48.0/20 permit 198.244.56.107 permit 198.244.56.108 permit @@ -1733,15 +1799,7 @@ 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.101.161.130 permit 199.101.162.0/25 permit 199.122.120.0/21 permit @@ -1798,11 +1856,11 @@ 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.168.0/21 permit 204.220.176.0/20 permit +204.220.181.105 permit 204.232.168.0/24 permit 205.139.110.0/24 permit 205.201.128.0/20 permit @@ -1830,7 +1888,6 @@ 207.46.52.79 permit 207.46.58.128/25 permit 207.46.116.128/29 permit -207.46.117.0/24 permit 207.46.132.128/27 permit 207.46.198.0/25 permit 207.46.200.0/27 permit @@ -1877,8 +1934,6 @@ 208.71.42.212/31 permit 208.71.42.214 permit 208.72.249.240/29 permit -208.74.204.5 permit -208.74.204.9 permit 208.75.120.0/22 permit 208.76.62.0/24 permit 208.76.63.0/24 permit @@ -1938,17 +1993,11 @@ 212.82.111.228/31 permit 212.82.111.230 permit 212.123.28.40 permit -212.227.15.3 permit -212.227.15.4 permit -212.227.15.5 permit -212.227.15.6 permit -212.227.15.14 permit +212.227.15.7 permit +212.227.15.8 permit 212.227.15.15 permit 212.227.15.18 permit 212.227.15.19 permit -212.227.15.25 permit -212.227.15.26 permit -212.227.15.29 permit 212.227.15.44 permit 212.227.15.45 permit 212.227.15.46 permit @@ -1956,23 +2005,32 @@ 212.227.15.50 permit 212.227.15.52 permit 212.227.15.53 permit -212.227.15.54 permit -212.227.15.55 permit -212.227.17.11 permit -212.227.17.12 permit -212.227.17.18 permit -212.227.17.19 permit +212.227.17.1 permit +212.227.17.2 permit +212.227.17.7 permit +212.227.17.16 permit +212.227.17.17 permit 212.227.17.20 permit 212.227.17.21 permit 212.227.17.22 permit 212.227.17.26 permit +212.227.17.27 permit 212.227.17.28 permit 212.227.17.29 permit +212.227.126.206 permit +212.227.126.207 permit +212.227.126.208 permit +212.227.126.209 permit +212.227.126.220 permit +212.227.126.221 permit +212.227.126.222 permit +212.227.126.223 permit 212.227.126.224 permit 212.227.126.225 permit 212.227.126.226 permit 212.227.126.227 permit -213.46.255.0/24 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 @@ -1982,6 +2040,7 @@ 216.17.150.242 permit 216.17.150.251 permit 216.24.224.0/20 permit +216.27.86.152/31 permit 216.39.60.154/31 permit 216.39.60.156/30 permit 216.39.60.160/30 permit @@ -2019,6 +2078,8 @@ 216.99.5.68 permit 216.109.114.32/27 permit 216.109.114.64/29 permit +216.113.162.65 permit +216.113.163.65 permit 216.128.126.97 permit 216.136.162.65 permit 216.136.162.120/29 permit @@ -2033,8 +2094,6 @@ 216.205.24.0/24 permit 216.221.160.0/19 permit 216.239.32.0/19 permit -217.72.192.77 permit -217.72.192.78 permit 217.77.141.52 permit 217.77.141.59 permit 217.175.194.0/24 permit @@ -2046,15 +2105,27 @@ 2001:0868:0100:0600::/64 permit 2001:4860:4000::/36 permit 2001:748:100:40::2:0/112 permit +2001:748:400:1300::3 permit +2001:748:400:1300::4 permit +2001:748:400:1301::0/64 permit +2001:748:400:1301::3 permit +2001:748:400:1301::4 permit +2001:748:400:2300::3 permit +2001:748:400:2300::4 permit +2001:748:400:2301::0/64 permit +2001:748:400:2301::3 permit +2001:748:400:2301::4 permit +2001:748:400:3300::3 permit +2001:748:400:3300::4 permit +2001:748:400:3301::0/64 permit +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 -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 2620:109:c003:104::/64 permit 2620:109:c003:104::215 permit @@ -2068,4 +2139,5 @@ 2620:119:50c0:207::/64 permit 2620:119:50c0:207::215 permit 2800:3f0:4000::/36 permit -194.25.134.0/24 permit # t-online.de +49.12.4.251 permit # checks.mailcow.email +2a01:4f8:c17:7906::10 permit # checks.mailcow.email diff --git a/mailcow/data/conf/rspamd/dynmaps/aliasexp.php b/mailcow/data/conf/rspamd/dynmaps/aliasexp.php index 824037c..814c0d3 100644 --- a/mailcow/data/conf/rspamd/dynmaps/aliasexp.php +++ b/mailcow/data/conf/rspamd/dynmaps/aliasexp.php @@ -133,7 +133,7 @@ try { error_log("ALIAS EXPANDER: 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` AND '1'"); + $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) { diff --git a/mailcow/data/conf/rspamd/dynmaps/settings.php b/mailcow/data/conf/rspamd/dynmaps/settings.php index 274e9da..1b0e6b6 100644 --- a/mailcow/data/conf/rspamd/dynmaps/settings.php +++ b/mailcow/data/conf/rspamd/dynmaps/settings.php @@ -56,7 +56,7 @@ function normalize_email($email) { $email = explode('@', $email); $email[0] = str_replace('.', '', $email[0]); $email = implode('@', $email); - } + } $gm_alt = "@googlemail.com"; if (substr_compare($email, $gm_alt, -strlen($gm_alt)) == 0) { $email = explode('@', $email); @@ -114,7 +114,7 @@ function ucl_rcpts($object, $type) { $rcpt[] = str_replace('/', '\/', $row['address']); } // Aliases by alias domains - $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox` + $stmt = $pdo->prepare("SELECT CONCAT(`local_part`, '@', `alias_domain`.`alias_domain`) AS `alias` FROM `mailbox` LEFT OUTER JOIN `alias_domain` ON `mailbox`.`domain` = `alias_domain`.`target_domain` WHERE `mailbox`.`username` = :object"); $stmt->execute(array( @@ -184,7 +184,7 @@ while ($row = array_shift($rows)) { rcpt = ; prepare("SELECT `option`, `value` FROM `filterconf` + $stmt = $pdo->prepare("SELECT `option`, `value` FROM `filterconf` WHERE (`option` = 'highspamlevel' OR `option` = 'lowspamlevel') AND `object`= :object"); $stmt->execute(array(':object' => $row['object'])); @@ -468,4 +468,36 @@ while ($row = array_shift($rows)) { + +query("SELECT `id`, `address`, `domain` FROM `alias` WHERE `active` = '1' AND `internal` = '1'"); +$aliases = $stmt->fetchAll(PDO::FETCH_ASSOC); +while ($alias = array_shift($aliases)) { + // build allowed_domains regex and add target domain and alias domains + $stmt = $pdo->prepare("SELECT `alias_domain` FROM `alias_domain` WHERE `active` = '1' AND `target_domain` = :target_domain"); + $stmt->execute(array(':target_domain' => $alias['domain'])); + $allowed_domains = $stmt->fetchAll(PDO::FETCH_ASSOC); + $allowed_domains = array_map(function($item) { + return str_replace('.', '\.', $item['alias_domain']); + }, $allowed_domains); + $allowed_domains[] = str_replace('.', '\.', $alias['domain']); + $allowed_domains = implode('|', $allowed_domains); +?> + internal_alias_ { + priority = 10; + rcpt = ""; + from = "/^((?!.*@()).)*$/"; + apply "default" { + MAILCOW_INTERNAL_ALIAS = 9999.0; + } + symbols [ + "MAILCOW_INTERNAL_ALIAS" + ] + } + + } diff --git a/mailcow/data/conf/rspamd/lua/rspamd.local.lua b/mailcow/data/conf/rspamd/lua/rspamd.local.lua index 5f23ef6..9d0a58b 100644 --- a/mailcow/data/conf/rspamd/lua/rspamd.local.lua +++ b/mailcow/data/conf/rspamd/lua/rspamd.local.lua @@ -102,7 +102,7 @@ rspamd_config:register_symbol({ local rcpt_split = rspamd_str_split(rcpt['addr'], '@') if #rcpt_split == 2 then if rcpt_split[1] == 'postmaster' then - task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt') + task:set_pre_result('accept', 'whitelisting postmaster smtp rcpt', 'postmaster') return end end @@ -167,7 +167,7 @@ rspamd_config:register_symbol({ for k,v in pairs(data) do if (v and v ~= userdata and v == '1') then rspamd_logger.infox(rspamd_config, "found ip in keep_spam map, setting pre-result") - task:set_pre_result('accept', 'ip matched with forward hosts') + task:set_pre_result('accept', 'ip matched with forward hosts', 'keep_spam') end end end @@ -454,12 +454,18 @@ rspamd_config:register_symbol({ local redis_params = rspamd_parse_redis_server('dyn_rl') local rspamd_logger = require "rspamd_logger" local envfrom = task:get_from(1) + local envrcpt = task:get_recipients(1) or {} local uname = task:get_user() if not envfrom or not uname then return false end + local uname = uname:lower() + if #envrcpt == 1 and envrcpt[1].addr:lower() == uname then + return false + end + local env_from_domain = envfrom[1].domain:lower() -- get smtp from domain in lower case local function redis_cb_user(err, data) @@ -544,13 +550,13 @@ rspamd_config:register_symbol({ -- determine newline type local function newline(task) local t = task:get_newlines_type() - + if t == 'cr' then return '\r' elseif t == 'lf' then return '\n' end - + return '\r\n' end -- retrieve footer @@ -558,7 +564,7 @@ rspamd_config:register_symbol({ if err or type(data) ~= 'string' then rspamd_logger.infox(rspamd_config, "domain wide footer request for user %s returned invalid or empty data (\"%s\") or error (\"%s\")", uname, data, err) else - + -- parse json string local footer = cjson.decode(data) if not footer then @@ -607,26 +613,30 @@ rspamd_config:register_symbol({ if footer.plain and footer.plain ~= "" then footer.plain = lua_util.jinja_template(footer.plain, replacements, true) end - + -- add footer local out = {} local rewrite = lua_mime.add_text_footer(task, footer.html, footer.plain) or {} - + local seen_cte local newline_s = newline(task) - + local function rewrite_ct_cb(name, hdr) if rewrite.need_rewrite_ct then if name:lower() == 'content-type' then - local nct = string.format('%s: %s/%s; charset=utf-8', - 'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype) + -- include boundary if present + local boundary_part = rewrite.new_ct.boundary and + string.format('; boundary="%s"', rewrite.new_ct.boundary) or '' + local nct = string.format('%s: %s/%s; charset=utf-8%s', + 'Content-Type', rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part) out[#out + 1] = nct - -- update Content-Type header + -- update Content-Type header (include boundary if present) task:set_milter_reply({ remove_headers = {['Content-Type'] = 0}, }) task:set_milter_reply({ - add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8', rewrite.new_ct.type, rewrite.new_ct.subtype)} + add_headers = {['Content-Type'] = string.format('%s/%s; charset=utf-8%s', + rewrite.new_ct.type, rewrite.new_ct.subtype, boundary_part)} }) return elseif name:lower() == 'content-transfer-encoding' then @@ -645,16 +655,16 @@ rspamd_config:register_symbol({ end out[#out + 1] = hdr.raw:gsub('\r?\n?$', '') end - + 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') end - + -- End of headers out[#out + 1] = newline_s - + if rewrite.out then for _,o in ipairs(rewrite.out) do out[#out + 1] = o diff --git a/mailcow/data/conf/rspamd/meta_exporter/pipe.php b/mailcow/data/conf/rspamd/meta_exporter/pipe.php index 4d8e2a1..003639b 100644 --- a/mailcow/data/conf/rspamd/meta_exporter/pipe.php +++ b/mailcow/data/conf/rspamd/meta_exporter/pipe.php @@ -182,7 +182,7 @@ foreach (json_decode($rcpts, true) as $rcpt) { error_log("RCPT RESOVLER: 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` AND '1'"); + $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) { @@ -236,6 +236,9 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) { ':action' => $action, ':fuzzy_hashes' => $fuzzy )); + $lastId = $pdo->lastInsertId(); + $stmt_update = $pdo->prepare("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE `id` = :id"); + $stmt_update->execute(array(':id' => $lastId)); $stmt = $pdo->prepare('DELETE FROM `quarantine` WHERE `rcpt` = :rcpt AND `id` NOT IN ( SELECT `id` FROM ( diff --git a/mailcow/data/conf/rspamd/meta_exporter/pushover.php b/mailcow/data/conf/rspamd/meta_exporter/pushover.php index af1b21e..efe5fdd 100644 --- a/mailcow/data/conf/rspamd/meta_exporter/pushover.php +++ b/mailcow/data/conf/rspamd/meta_exporter/pushover.php @@ -167,7 +167,7 @@ foreach (json_decode($rcpts, true) as $rcpt) { error_log("RCPT RESOVLER: 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` AND '1'"); + $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) { diff --git a/mailcow/data/conf/sogo/sogo.conf b/mailcow/data/conf/sogo/sogo.conf index 8455f0c..6854569 100644 --- a/mailcow/data/conf/sogo/sogo.conf +++ b/mailcow/data/conf/sogo/sogo.conf @@ -16,6 +16,9 @@ SOGoFoldersSendEMailNotifications = YES; SOGoForwardEnabled = YES; + // Added with SOGo 5.12 - Allows users to cleanup there maildirectories by deleting mails oder than X + SOGoEnableMailCleaning = YES; + // Fixes "MODIFICATION_FAILED" error (HTTP 412) in Clients when accepting invitations from external services SOGoDisableOrganizerEventCheck = YES; @@ -83,6 +86,12 @@ SOGoMaximumFailedLoginInterval = 900; SOGoFailedLoginBlockInterval = 900; + // Enable SOGo URL Description for GDPR compliance, this may cause some issues with calendars and contacts. Also uncomment the encryption key below to use it. + //SOGoURLEncryptionEnabled = NO; + + // Set a 16 character encryption key for SOGo URL Description, change this to your own value + //SOGoURLPathEncryptionKey = "SOGoSuperSecret0"; + GCSChannelCollectionTimer = 60; GCSChannelExpireAge = 60; @@ -91,7 +100,7 @@ //SoDebugBaseURL = YES; //ImapDebugEnabled = YES; //SOGoEASDebugEnabled = YES; - SOGoEASSearchInBody = YES; // Experimental. Enabled since 2023-10 + SOGoEASSearchInBody = YES; //LDAPDebugEnabled = YES; //PGDebugEnabled = YES; //MySQL4DebugEnabled = YES; diff --git a/mailcow/data/web/admin/dashboard.php b/mailcow/data/web/admin/dashboard.php index 473f28d..443c2ea 100644 --- a/mailcow/data/web/admin/dashboard.php +++ b/mailcow/data/web/admin/dashboard.php @@ -18,6 +18,7 @@ elseif (!isset($_SESSION['mailcow_cc_role']) || $_SESSION['mailcow_cc_role'] != require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/header.inc.php'; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; $clamd_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_CLAMD"])) ? false : true; +$olefy_status = (preg_match("/^([yY][eE][sS]|[yY])+$/", $_ENV["SKIP_OLEFY"])) ? false : true; if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CACHE')) { @@ -33,6 +34,7 @@ $vmail_df = explode(',', (string)json_decode(docker('post', 'dovecot-mailcow', ' // containers $containers_info = (array) docker('info'); if ($clamd_status === false) unset($containers_info['clamd-mailcow']); +if ($olefy_status === false) unset($containers_info['olefy-mailcow']); ksort($containers_info); $containers = array(); foreach ($containers_info as $container => $container_info) { @@ -77,6 +79,7 @@ $template_data = [ 'gal' => @$_SESSION['gal'], 'license_guid' => license('guid'), 'clamd_status' => $clamd_status, + 'olefy_status' => $olefy_status, 'containers' => $containers, 'ip_check' => customize('get', 'ip_check'), 'lang_admin' => json_encode($lang['admin']), diff --git a/mailcow/data/web/admin/index.php b/mailcow/data/web/admin/index.php index 05ba703..9ae4a03 100644 --- a/mailcow/data/web/admin/index.php +++ b/mailcow/data/web/admin/index.php @@ -22,7 +22,8 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING']; $template = 'admin_index.twig'; $template_data = [ - 'login_delay' => @$_SESSION['ldelay'] + 'login_delay' => @$_SESSION['ldelay'], + 'custom_login' => customize('get', 'custom_login'), ]; $js_minifier->add('/web/js/site/index.js'); diff --git a/mailcow/data/web/admin/system.php b/mailcow/data/web/admin/system.php index c21d43f..9fd44e0 100644 --- a/mailcow/data/web/admin/system.php +++ b/mailcow/data/web/admin/system.php @@ -125,6 +125,7 @@ $template_data = [ 'logo_specs' => customize('get', 'main_logo_specs'), 'logo_dark_specs' => customize('get', 'main_logo_dark_specs'), 'ip_check' => customize('get', 'ip_check'), + 'custom_login' => customize('get', 'custom_login'), 'password_complexity' => password_complexity('get'), 'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'], 'cors_settings' => $cors_settings, diff --git a/mailcow/data/web/api/openapi.yaml b/mailcow/data/web/api/openapi.yaml index afab5fb..f207ee6 100644 --- a/mailcow/data/web/api/openapi.yaml +++ b/mailcow/data/web/api/openapi.yaml @@ -5847,6 +5847,7 @@ paths: client_id: "mailcow_client" client_secret: "*" redirect_url: "https://mail.mailcow.tld" + redirect_url_extra: ["https://extramail.mailcow.tld"] version: "26.1.3" default_template: "Default" mappers: @@ -5900,6 +5901,9 @@ paths: redirect_url: description: The redirect URL that OIDC Provider will use after authentication. Required if `authsource` is keycloak or generic-oidc. type: string + redirect_url_extra: + description: Additional redirect URLs that OIDC Provider can use after authentication if valid. + type: array version: description: Specifies the Keycloak version. Required if `authsource` is keycloak. type: string @@ -5990,6 +5994,7 @@ paths: client_id: "mailcow_client" client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf" redirect_url: "https://mail.mailcow.tld" + redirect_url_extra: ["https://extramail.mailcow.tld"] version: "26.1.3" default_template: "Default" mappers: ["small_mbox", "medium_mbox"] @@ -6034,6 +6039,7 @@ paths: client_id: "mailcow_client" client_secret: "Xy7GdPqvJ9m3R8sT2LkVZ5W1oNbCaYQf" redirect_url: "https://mail.mailcow.tld" + redirect_url_extra: ["https://extramail.mailcow.tld"] client_scopes: "openid profile email mailcow_template" default_template: "Default" mappers: ["small_mbox", "medium_mbox"] diff --git a/mailcow/data/web/autoconfig.php b/mailcow/data/web/autoconfig.php index 95952df..6a528d4 100644 --- a/mailcow/data/web/autoconfig.php +++ b/mailcow/data/web/autoconfig.php @@ -85,7 +85,7 @@ if (count($records) == 0 || $records[0]['target'] != '') { ?> password-cleartext - + If you didn't change the password given to you by the administrator or if you didn't change it in a long time, please consider doing that now. Sollten Sie das Ihnen durch den Administrator vergebene Passwort noch nicht geändert haben, empfehlen wir dies nun zu tun. Auch ein altes Passwort sollte aus Sicherheitsgründen geändert werden. diff --git a/mailcow/data/web/autodiscover.php b/mailcow/data/web/autodiscover.php index 9526313..224f94f 100644 --- a/mailcow/data/web/autodiscover.php +++ b/mailcow/data/web/autodiscover.php @@ -7,6 +7,8 @@ if(file_exists('inc/vars.local.inc.php')) { require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.auth.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php'; +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.ratelimit.inc.php'; $default_autodiscover_config = $autodiscover_config; $autodiscover_config = array_merge($default_autodiscover_config, $autodiscover_config); diff --git a/mailcow/data/web/domainadmin/index.php b/mailcow/data/web/domainadmin/index.php index 2d909f9..0d70ec3 100644 --- a/mailcow/data/web/domainadmin/index.php +++ b/mailcow/data/web/domainadmin/index.php @@ -22,6 +22,7 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING']; $template = 'domainadmin_index.twig'; $template_data = [ 'login_delay' => @$_SESSION['ldelay'], + 'custom_login' => customize('get', 'custom_login'), ]; $js_minifier->add('/web/js/site/index.js'); diff --git a/mailcow/data/web/edit.php b/mailcow/data/web/edit.php index 9a1e5a5..57cf24b 100644 --- a/mailcow/data/web/edit.php +++ b/mailcow/data/web/edit.php @@ -48,6 +48,12 @@ if (isset($_SESSION['mailcow_cc_role'])) { $rl = ratelimit('get', 'domain', $domain); $rlyhosts = relayhost('get'); $domain_footer = mailbox('get', 'domain_wide_footer', $domain); + $mta_sts = mailbox('get', 'mta_sts', $domain); + if (count($mta_sts) == 0) { + $mta_sts = false; + } elseif (isset($mta_sts['mx'])) { + $mta_sts['mx'] = implode(',', $mta_sts['mx']); + } $template = 'edit/domain.twig'; $template_data = [ 'acl' => $_SESSION['acl'], @@ -58,6 +64,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { 'dkim' => dkim('details', $domain), 'domain_details' => $result, 'domain_footer' => $domain_footer, + 'mta_sts' => $mta_sts, 'mailboxes' => mailbox('get', 'mailboxes', $_GET["domain"]), 'aliases' => mailbox('get', 'aliases', $_GET["domain"], 'address'), 'alias_domains' => mailbox('get', 'alias_domains', $_GET["domain"]) @@ -125,6 +132,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { 'mailbox' => $mailbox, 'rl' => $rl, 'pushover_data' => $pushover_data, + 'get_tagging_options' => mailbox('get', 'delimiter_action', $mailbox), 'quarantine_notification' => $quarantine_notification, 'quarantine_category' => $quarantine_category, 'get_tls_policy' => $get_tls_policy, diff --git a/mailcow/data/web/favicon.png b/mailcow/data/web/favicon.png index 69eb2fc..5413613 100644 Binary files a/mailcow/data/web/favicon.png and b/mailcow/data/web/favicon.png differ diff --git a/mailcow/data/web/inc/ajax/dns_diagnostics.php b/mailcow/data/web/inc/ajax/dns_diagnostics.php index 15cb3a3..b48239e 100644 --- a/mailcow/data/web/inc/ajax/dns_diagnostics.php +++ b/mailcow/data/web/inc/ajax/dns_diagnostics.php @@ -71,6 +71,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm // Init records array $spf_link = 'SPF Record Syntax
'; $dmarc_link = 'DMARC Assistant'; + $mtasts_report_link = 'TLS Report Record Syntax'; $records = array(); @@ -128,6 +129,27 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm ); } + $mta_sts = mailbox('get', 'mta_sts', $domain); + if (count($mta_sts) > 0 && $mta_sts['active'] == 1) { + if (!in_array($domain, $alias_domains)) { + $records[] = array( + 'mta-sts.' . $domain, + 'CNAME', + $mailcow_hostname + ); + } + $records[] = array( + '_mta-sts.' . $domain, + 'TXT', + "v={$mta_sts['version']};id={$mta_sts['id']};", + ); + $records[] = array( + '_smtp._tls.' . $domain, + 'TXT', + $mtasts_report_link, + ); + } + $records[] = array( $domain, 'TXT', @@ -341,15 +363,25 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm } foreach ($currents as &$current) { + if ($current['type'] == "TXT" && + stripos(strtolower($current['txt']), 'v=sts') === 0) { + if (strtolower($current[$data_field[$current['type']]]) == strtolower($record[2])) { + $state = state_good; + } + else { + $state = state_nomatch; + } + $state .= '
' . $current[$data_field[$current['type']]]; + } if ($current['type'] == 'TXT' && - stripos($current['txt'], 'v=dmarc') === 0 && - $record[2] == $dmarc_link) { + stripos($current['txt'], 'v=dmarc') === 0 && + $record[2] == $dmarc_link) { $current['txt'] = str_replace(' ', '', $current['txt']); $state = $current[$data_field[$current['type']]] . state_optional; } elseif ($current['type'] == 'TXT' && - stripos($current['txt'], 'v=spf') === 0 && - $record[2] == $spf_link) { + stripos($current['txt'], 'v=spf') === 0 && + $record[2] == $spf_link) { $state = state_nomatch; $rslt = get_spf_allowed_hosts($record[0], true); if (in_array($ip, $rslt) && in_array(expand_ipv6($ip6), $rslt)) { @@ -358,8 +390,8 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm $state .= '
' . $current[$data_field[$current['type']]] . state_optional; } elseif ($current['type'] == 'TXT' && - stripos($current['txt'], 'v=dkim') === 0 && - stripos($record[2], 'v=dkim') === 0) { + stripos($current['txt'], 'v=dkim') === 0 && + stripos($record[2], 'v=dkim') === 0) { preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $current[$data_field[$current['type']]], $dkim_matches_current); preg_match('/v=DKIM1;.*k=rsa;.*p=([^;]*).*/i', $record[2], $dkim_matches_good); if ($dkim_matches_current[1] == $dkim_matches_good[1]) { @@ -367,7 +399,7 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm } } elseif ($current['type'] != 'TXT' && - isset($data_field[$current['type']]) && $state != state_good) { + isset($data_field[$current['type']]) && $state != state_good) { $state = state_nomatch; if ($current[$data_field[$current['type']]] == $record[2]) { $state = state_good; diff --git a/mailcow/data/web/inc/footer.inc.php b/mailcow/data/web/inc/footer.inc.php index ac1bff0..ecc4ddc 100644 --- a/mailcow/data/web/inc/footer.inc.php +++ b/mailcow/data/web/inc/footer.inc.php @@ -26,23 +26,25 @@ if (is_array($alertbox_log_parser)) { // map tfa details for twig $pending_tfa_authmechs = []; -foreach($_SESSION['pending_tfa_methods'] as $authdata){ - $pending_tfa_authmechs[$authdata['authmech']] = false; -} -if (isset($pending_tfa_authmechs['webauthn'])) { - $pending_tfa_authmechs['webauthn'] = true; -} -if (!isset($pending_tfa_authmechs['webauthn']) - && isset($pending_tfa_authmechs['yubi_otp'])) { - $pending_tfa_authmechs['yubi_otp'] = true; -} -if (!isset($pending_tfa_authmechs['webauthn']) - && !isset($pending_tfa_authmechs['yubi_otp']) - && isset($pending_tfa_authmechs['totp'])) { - $pending_tfa_authmechs['totp'] = true; -} -if (isset($pending_tfa_authmechs['u2f'])) { - $pending_tfa_authmechs['u2f'] = true; +if (array_key_exists('pending_tfa_methods', $_SESSION)) { + foreach($_SESSION['pending_tfa_methods'] as $authdata){ + $pending_tfa_authmechs[$authdata['authmech']] = false; + } + if (isset($pending_tfa_authmechs['webauthn'])) { + $pending_tfa_authmechs['webauthn'] = true; + } + if (!isset($pending_tfa_authmechs['webauthn']) + && isset($pending_tfa_authmechs['yubi_otp'])) { + $pending_tfa_authmechs['yubi_otp'] = true; + } + if (!isset($pending_tfa_authmechs['webauthn']) + && !isset($pending_tfa_authmechs['yubi_otp']) + && isset($pending_tfa_authmechs['totp'])) { + $pending_tfa_authmechs['totp'] = true; + } + if (isset($pending_tfa_authmechs['u2f'])) { + $pending_tfa_authmechs['u2f'] = true; + } } // globals diff --git a/mailcow/data/web/inc/functions.app_passwd.inc.php b/mailcow/data/web/inc/functions.app_passwd.inc.php index b493fc9..aacbb82 100644 --- a/mailcow/data/web/inc/functions.app_passwd.inc.php +++ b/mailcow/data/web/inc/functions.app_passwd.inc.php @@ -1,7 +1,7 @@ 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'password_complexity' - ); - return false; - } - if ($password != $password2) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'password_mismatch' - ); + if (password_check($password, $password2) !== true) { return false; } $password_hashed = hash_password($password); @@ -88,15 +75,15 @@ function app_passwd($_action, $_data = null) { 'log' => array(__FUNCTION__, $_action, $_data_log), 'msg' => 'app_passwd_added' ); - break; + break; case 'edit': $ids = (array)$_data['id']; foreach ($ids as $id) { $is_now = app_passwd('details', $id); if (!empty($is_now)) { $app_name = (!empty($_data['app_name'])) ? $_data['app_name'] : $is_now['name']; - $password = (!empty($_data['password'])) ? $_data['password'] : null; - $password2 = (!empty($_data['password2'])) ? $_data['password2'] : null; + $password = (!empty($_data['app_passwd'])) ? $_data['app_passwd'] : null; + $password2 = (!empty($_data['app_passwd2'])) ? $_data['app_passwd2'] : null; if (isset($_data['protocols'])) { $protocols = (array)$_data['protocols']; $imap_access = (in_array('imap_access', $protocols)) ? 1 : 0; @@ -126,20 +113,7 @@ function app_passwd($_action, $_data = null) { } $app_name = htmlspecialchars(trim($app_name)); if (!empty($password) && !empty($password2)) { - if (!preg_match('/' . $GLOBALS['PASSWD_REGEP'] . '/', $password)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'password_complexity' - ); - continue; - } - if ($password != $password2) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'password_mismatch' - ); + if (password_check($password, $password2) !== true) { continue; } $password_hashed = hash_password($password); @@ -182,7 +156,7 @@ function app_passwd($_action, $_data = null) { 'msg' => array('object_modified', htmlspecialchars(implode(', ', $ids))) ); } - break; + break; case 'delete': $ids = (array)$_data['id']; foreach ($ids as $id) { @@ -213,19 +187,17 @@ function app_passwd($_action, $_data = null) { 'msg' => array('app_passwd_removed', htmlspecialchars($id)) ); } - break; + break; case 'get': $app_passwds = array(); $stmt = $pdo->prepare("SELECT `id`, `name` FROM `app_passwd` WHERE `mailbox` = :username"); $stmt->execute(array(':username' => $username)); $app_passwds = $stmt->fetchAll(PDO::FETCH_ASSOC); return $app_passwds; - break; + break; case 'details': $app_passwd_data = array(); - $stmt = $pdo->prepare("SELECT * - FROM `app_passwd` - WHERE `id` = :id"); + $stmt = $pdo->prepare("SELECT * FROM `app_passwd` WHERE `id` = :id"); $stmt->execute(array(':id' => $_data)); $app_passwd_data = $stmt->fetch(PDO::FETCH_ASSOC); if (empty($app_passwd_data)) { @@ -237,6 +209,6 @@ function app_passwd($_action, $_data = null) { } $app_passwd_data['name'] = htmlspecialchars(trim($app_passwd_data['name'])); return $app_passwd_data; - break; + break; } } diff --git a/mailcow/data/web/inc/functions.auth.inc.php b/mailcow/data/web/inc/functions.auth.inc.php index 994915e..059dd4c 100644 --- a/mailcow/data/web/inc/functions.auth.inc.php +++ b/mailcow/data/web/inc/functions.auth.inc.php @@ -193,6 +193,7 @@ function user_login($user, $pass, $extra = null){ global $iam_settings; $is_internal = $extra['is_internal']; + $service = $extra['service']; if (!filter_var($user, FILTER_VALIDATE_EMAIL) && !ctype_alnum(str_replace(array('_', '.', '-'), '', $user))) { if (!$is_internal){ @@ -235,6 +236,14 @@ function user_login($user, $pass, $extra = null){ $row = $stmt->fetch(PDO::FETCH_ASSOC); if (!empty($row)) { + // check if user has access to service (imap, smtp, pop3, sieve) if service is set + $row['attributes'] = json_decode($row['attributes'], true); + if (isset($service)) { + $key = strtolower($service) . "_access"; + if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') { + return false; + } + } return true; } } @@ -242,6 +251,14 @@ function user_login($user, $pass, $extra = null){ return false; } + // check if user has access to service (imap, smtp, pop3, sieve) if service is set + $row['attributes'] = json_decode($row['attributes'], true); + if (isset($service)) { + $key = strtolower($service) . "_access"; + if (isset($row['attributes'][$key]) && $row['attributes'][$key] != '1') { + return false; + } + } switch ($row['authsource']) { case 'keycloak': // user authsource is keycloak, try using via rest flow @@ -351,6 +368,11 @@ function user_login($user, $pass, $extra = null){ } // verify password if (verify_hash($row['password'], $pass) !== false) { + + if (intval($row['attributes']['force_pw_update']) == 1) { + $_SESSION['pending_pw_update'] = true; + } + // check for tfa authenticators $authenticators = get_tfa($user); if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0 && !$is_internal) { @@ -471,6 +493,9 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){ } return false; } + if (!$iam_provider) { + return false; + } // get access_token for service account of mailcow client $admin_token = identity_provider("get-keycloak-admin-token"); @@ -540,6 +565,17 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){ return 'user'; } + // check if login provisioning is enabled before creating user + if (!$iam_settings['login_provisioning']){ + if (!$is_internal){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"), + 'msg' => 'login_failed' + ); + } + return false; + } // check if matching attribute exist if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) { if (!empty($iam_settings['default_template'])) { @@ -653,10 +689,21 @@ function ldap_mbox_login($user, $pass, $extra = null){ return 'user'; } + // check if login provisioning is enabled before creating user + if (!$iam_settings['login_provisioning']){ + if (!$is_internal){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"), + 'msg' => 'login_failed' + ); + } + return false; + } // check if matching attribute exist if (empty($iam_settings['mappers']) || !$user_template || $mapper_key === false) { - if (!empty($iam_settings['default_tempalte'])) { - $mbox_template = $iam_settings['default_tempalte']; + if (!empty($iam_settings['default_template'])) { + $mbox_template = $iam_settings['default_template']; } else { $_SESSION['return'][] = array( 'type' => 'danger', diff --git a/mailcow/data/web/inc/functions.customize.inc.php b/mailcow/data/web/inc/functions.customize.inc.php index 8c0feb6..b396eec 100644 --- a/mailcow/data/web/inc/functions.customize.inc.php +++ b/mailcow/data/web/inc/functions.customize.inc.php @@ -204,6 +204,35 @@ function customize($_action, $_item, $_data = null) { 'msg' => 'ip_check_opt_in_modified' ); break; + case 'custom_login': + $hide_user_quicklink = ($_data['hide_user_quicklink'] == "1") ? 1 : 0; + $hide_domainadmin_quicklink = ($_data['hide_domainadmin_quicklink'] == "1") ? 1 : 0; + $hide_admin_quicklink = ($_data['hide_admin_quicklink'] == "1") ? 1 : 0; + $force_sso = ($_data['force_sso'] == "1") ? 1 : 0; + + $custom_login = array( + "hide_user_quicklink" => $hide_user_quicklink, + "hide_domainadmin_quicklink" => $hide_domainadmin_quicklink, + "hide_admin_quicklink" => $hide_admin_quicklink, + "force_sso" => $force_sso, + ); + try { + $redis->set('CUSTOM_LOGIN', json_encode($custom_login)); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => 'custom_login_modified' + ); + break; } break; case 'delete': @@ -264,7 +293,7 @@ function customize($_action, $_item, $_data = null) { } if (empty($app_links)){ - return false; + return []; } // convert from old style @@ -296,8 +325,10 @@ function customize($_action, $_item, $_data = null) { break; case 'ui_texts': try { - $data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : 'mailcow UI'; - $data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : 'mailcow UI'; + $mailcow_hostname = strtolower(getenv("MAILCOW_HOSTNAME")); + + $data['title_name'] = ($title_name = $redis->get('TITLE_NAME')) ? $title_name : "$mailcow_hostname - mail UI"; + $data['main_name'] = ($main_name = $redis->get('MAIN_NAME')) ? $main_name : "$mailcow_hostname - mail UI"; $data['apps_name'] = ($apps_name = $redis->get('APPS_NAME')) ? $apps_name : $lang['header']['apps']; $data['help_text'] = ($help_text = $redis->get('HELP_TEXT')) ? $help_text : false; if (!empty($redis->get('UI_IMPRESS'))) { @@ -357,6 +388,20 @@ function customize($_action, $_item, $_data = null) { return false; } break; + case 'custom_login': + try { + $custom_login = $redis->get('CUSTOM_LOGIN'); + return $custom_login ? json_decode($custom_login, true) : array(); + } + catch (RedisException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_item, $_data), + 'msg' => array('redis_error', $e) + ); + return false; + } + break; } break; } diff --git a/mailcow/data/web/inc/functions.inc.php b/mailcow/data/web/inc/functions.inc.php index 49e26b9..1947ec4 100644 --- a/mailcow/data/web/inc/functions.inc.php +++ b/mailcow/data/web/inc/functions.inc.php @@ -814,6 +814,32 @@ function verify_hash($hash, $password) { $hash = $components[4]; return hash_equals(hash_pbkdf2('sha1', $password, $salt, $rounds), $hash); + case "PBKDF2-SHA512": + // Handle FreeIPA-style hash: {PBKDF2-SHA512}10000$$ + $components = explode('$', $hash); + if (count($components) !== 3) return false; + + // 1st part: iteration count (integer) + $iterations = intval($components[0]); + if ($iterations <= 0) return false; + + // 2nd part: salt (base64-encoded) + $salt = $components[1]; + // 3rd part: hash (base64-encoded) + $stored_hash_b64 = $components[2]; + + // Decode salt and hash from base64 + $salt_bin = base64_decode($salt, true); + $hash_bin = base64_decode($stored_hash_b64, true); + if ($salt_bin === false || $hash_bin === false) return false; + // Get length of hash in bytes + $hash_len = strlen($hash_bin); + if ($hash_len === 0) return false; + + // Calculate PBKDF2-SHA512 hash for provided password + $test_hash = hash_pbkdf2('sha512', $password, $salt_bin, $iterations, $hash_len, true); + return hash_equals($hash_bin, $test_hash); + case "PLAIN-MD4": return hash_equals(hash('md4', $password), $hash); @@ -1001,11 +1027,12 @@ function edit_user_account($_data) { ':password_hashed' => $password_hashed, ':username' => $username )); + $_SESSION['pending_pw_update'] = false; update_sogo_static_view(); } // edit password recovery email - elseif (isset($pw_recovery_email)) { + elseif (!empty($password_old) && isset($pw_recovery_email)) { if (!isset($_SESSION['acl']['pw_reset']) || $_SESSION['acl']['pw_reset'] != "1" ) { $_SESSION['return'][] = array( 'type' => 'danger', @@ -1015,6 +1042,21 @@ function edit_user_account($_data) { return false; } + $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `username` = :user AND authsource = 'mailcow'"); + $stmt->execute(array(':user' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if (!verify_hash($row['password'], $password_old)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + $pw_recovery_email = (!filter_var($pw_recovery_email, FILTER_VALIDATE_EMAIL)) ? '' : $pw_recovery_email; $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.recovery_email', :recovery_email) WHERE `username` = :username AND authsource = 'mailcow'"); @@ -1106,11 +1148,21 @@ function user_get_alias_details($username) { } return $data; } -function is_valid_domain_name($domain_name) { +function is_valid_domain_name($domain_name, $options = array()) { if (empty($domain_name)) { return false; } + + // Convert domain name to ASCII for validation $domain_name = idn_to_ascii($domain_name, 0, INTL_IDNA_VARIANT_UTS46); + + if (isset($options['allow_wildcard']) && $options['allow_wildcard'] == true) { + // Remove '*.' if wildcard subdomains are allowed + if (strpos($domain_name, '*.') === 0) { + $domain_name = substr($domain_name, 2); + } + } + return (preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*$/i", $domain_name) && preg_match("/^.{1,253}$/", $domain_name) && preg_match("/^[^\.]{1,63}(\.[^\.]{1,63})*$/", $domain_name)); @@ -2210,7 +2262,7 @@ function cors($action, $data = null) { $cors_settings['allowed_origins'] = $allowed_origins[0]; if (in_array('*', $allowed_origins)){ $cors_settings['allowed_origins'] = '*'; - } else if (in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) { + } else if (array_key_exists('HTTP_ORIGIN', $_SERVER) && in_array($_SERVER['HTTP_ORIGIN'], $allowed_origins)) { $cors_settings['allowed_origins'] = $_SERVER['HTTP_ORIGIN']; } // always allow OPTIONS for preflight request @@ -2286,12 +2338,14 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach($rows as $row){ switch ($row["key"]) { + case "redirect_url_extra": case "mappers": case "templates": $settings[$row["key"]] = json_decode($row["value"]); break; case "use_ssl": case "use_tls": + case "login_provisioning": case "ignore_ssl_errors": $settings[$row["key"]] = boolval($row["value"]); break; @@ -2300,6 +2354,10 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { break; } } + // set login_provisioning if not exists + if (!array_key_exists('login_provisioning', $settings)) { + $settings['login_provisioning'] = 1; + } // return default client_scopes for generic-oidc if none is set if ($settings["authsource"] == "generic-oidc" && empty($settings["client_scopes"])){ $settings["client_scopes"] = "openid profile email mailcow_template"; @@ -2364,7 +2422,8 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { return false; } - $_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false; + $_data['ignore_ssl_error'] = isset($_data['ignore_ssl_error']) ? boolval($_data['ignore_ssl_error']) : false; + $_data['login_provisioning'] = isset($_data['login_provisioning']) ? boolval($_data['login_provisioning']) : false; switch ($_data['authsource']) { case "keycloak": $_data['server_url'] = (!empty($_data['server_url'])) ? rtrim($_data['server_url'], '/') : null; @@ -2373,14 +2432,14 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { $_data['import_users'] = isset($_data['import_users']) ? intval($_data['import_users']) : 0; $_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15; $_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval']; - $required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error'); + $required_settings = array('authsource', 'server_url', 'realm', 'client_id', 'client_secret', 'redirect_url', 'version', 'mailpassword_flow', 'periodic_sync', 'import_users', 'sync_interval', 'ignore_ssl_error', 'login_provisioning'); break; case "generic-oidc": $_data['authorize_url'] = (!empty($_data['authorize_url'])) ? $_data['authorize_url'] : null; $_data['token_url'] = (!empty($_data['token_url'])) ? $_data['token_url'] : null; $_data['userinfo_url'] = (!empty($_data['userinfo_url'])) ? $_data['userinfo_url'] : null; $_data['client_scopes'] = (!empty($_data['client_scopes'])) ? $_data['client_scopes'] : "openid profile email mailcow_template"; - $required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes', 'ignore_ssl_error'); + $required_settings = array('authsource', 'authorize_url', 'token_url', 'client_id', 'client_secret', 'redirect_url', 'userinfo_url', 'client_scopes', 'ignore_ssl_error', 'login_provisioning'); break; case "ldap": $_data['host'] = (!empty($_data['host'])) ? str_replace(" ", "", $_data['host']) : ""; @@ -2394,7 +2453,7 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { $_data['use_tls'] = isset($_data['use_tls']) && !$_data['use_ssl'] ? boolval($_data['use_tls']) : false; $_data['sync_interval'] = (!empty($_data['sync_interval'])) ? intval($_data['sync_interval']) : 15; $_data['sync_interval'] = $_data['sync_interval'] < 1 ? 1 : $_data['sync_interval']; - $required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error'); + $required_settings = array('authsource', 'host', 'port', 'basedn', 'username_field', 'filter', 'attribute_field', 'binddn', 'bindpass', 'periodic_sync', 'import_users', 'sync_interval', 'use_ssl', 'use_tls', 'ignore_ssl_error', 'login_provisioning'); break; } @@ -2418,6 +2477,18 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { } $pdo->commit(); + // add redirect_url_extra + if (isset($_data['redirect_url_extra'])){ + $_data['redirect_url_extra'] = (!is_array($_data['redirect_url_extra'])) ? array($_data['redirect_url_extra']) : $_data['redirect_url_extra']; + + $redirect_url_extra = array_filter($_data['redirect_url_extra']); + $redirect_url_extra = json_encode($redirect_url_extra); + + $stmt = $pdo->prepare("INSERT INTO identity_provider (`key`, `value`) VALUES ('redirect_url_extra', :value) ON DUPLICATE KEY UPDATE `value` = VALUES(`value`);"); + $stmt->bindParam(':value', $redirect_url_extra); + $stmt->execute(); + } + // add default template if (isset($_data['default_template'])) { $_data['default_template'] = (empty($_data['default_template'])) ? "" : $_data['default_template']; @@ -2752,6 +2823,16 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { return true; } + // user doesn't exist, check if login provisioning is enabled + if (!$iam_settings['login_provisioning']){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, "Auto-create users on login is deactivated"), + 'msg' => 'login_failed' + ); + return false; + } + if (empty($iam_settings['mappers']) || empty($user_template) || $mapper_key === false){ if (!empty($iam_settings['default_template'])) { $mbox_template = $iam_settings['default_template']; @@ -2851,7 +2932,19 @@ function identity_provider($_action = null, $_data = null, $_extra = null) { case "get-redirect": if ($iam_settings['authsource'] != 'keycloak' && $iam_settings['authsource'] != 'generic-oidc') return false; - $authUrl = $iam_provider->getAuthorizationUrl(); + $options = []; + if (isset($iam_settings['redirect_url_extra'])) { + // check if the current domain is used in an extra redirect URL + $targetDomain = strtolower($_SERVER['HTTP_HOST']); + foreach ($iam_settings['redirect_url_extra'] as $testUrl) { + $testUrlParsed = parse_url($testUrl); + if (isset($testUrlParsed['host']) && strtolower($testUrlParsed['host']) == $targetDomain) { + $options['redirect_uri'] = $testUrl; + break; + } + } + } + $authUrl = $iam_provider->getAuthorizationUrl($options); $_SESSION['oauth2state'] = $iam_provider->getState(); return $authUrl; break; diff --git a/mailcow/data/web/inc/functions.mailbox.inc.php b/mailcow/data/web/inc/functions.mailbox.inc.php index d5daedd..d8e4e17 100644 --- a/mailcow/data/web/inc/functions.mailbox.inc.php +++ b/mailcow/data/web/inc/functions.mailbox.inc.php @@ -49,6 +49,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // Default to 1 yr $_data["validity"] = 8760; } + if (isset($_data["permanent"]) && filter_var($_data["permanent"], FILTER_VALIDATE_BOOL)) { + $permanent = 1; + } + else { + $permanent = 0; + } $domain = $_data['domain']; $description = $_data['description']; $valid_domains[] = mailbox('get', 'mailbox_details', $username)['domain']; @@ -65,13 +71,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return false; } $validity = strtotime("+" . $_data["validity"] . " hour"); - $stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`) VALUES - (:address, :description, :goto, :validity)"); + $stmt = $pdo->prepare("INSERT INTO `spamalias` (`address`, `description`, `goto`, `validity`, `permanent`) VALUES + (:address, :description, :goto, :validity, :permanent)"); $stmt->execute(array( ':address' => readable_random_string(rand(rand(3, 9), rand(3, 9))) . '.' . readable_random_string(rand(rand(3, 9), rand(3, 9))) . '@' . $domain, ':description' => $description, ':goto' => $username, - ':validity' => $validity + ':validity' => $validity, + ':permanent' => $permanent )); $_SESSION['return'][] = array( 'type' => 'success', @@ -684,15 +691,16 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return true; break; case 'alias': - $addresses = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['address'])); - $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto'])); - $active = intval($_data['active']); - $sogo_visible = intval($_data['sogo_visible']); - $goto_null = intval($_data['goto_null']); - $goto_spam = intval($_data['goto_spam']); - $goto_ham = intval($_data['goto_ham']); + $addresses = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['address'])); + $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $_data['goto'])); + $internal = intval($_data['internal']); + $active = intval($_data['active']); + $sogo_visible = intval($_data['sogo_visible']); + $goto_null = intval($_data['goto_null']); + $goto_spam = intval($_data['goto_spam']); + $goto_ham = intval($_data['goto_ham']); $private_comment = $_data['private_comment']; - $public_comment = $_data['public_comment']; + $public_comment = $_data['public_comment']; if (strlen($private_comment) > 160 | strlen($public_comment) > 160){ $_SESSION['return'][] = array( 'type' => 'danger', @@ -842,8 +850,8 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `active`) - VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :active)"); + $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `public_comment`, `private_comment`, `goto`, `domain`, `sogo_visible`, `internal`, `active`) + VALUES (:address, :public_comment, :private_comment, :goto, :domain, :sogo_visible, :internal, :active)"); if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) { $stmt->execute(array( ':address' => '@'.$domain, @@ -853,6 +861,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':goto' => $goto, ':domain' => $domain, ':sogo_visible' => $sogo_visible, + ':internal' => $internal, ':active' => $active )); } @@ -864,6 +873,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':goto' => $goto, ':domain' => $domain, ':sogo_visible' => $sogo_visible, + ':internal' => $internal, ':active' => $active )); } @@ -1223,6 +1233,14 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':username' => $username )); + // save delimiter_action + if (isset($_data['tagged_mail_handler'])) { + mailbox('edit', 'delimiter_action', array( + 'username' => $username, + 'tagged_mail_handler' => $_data['tagged_mail_handler'] + )); + } + // save tags foreach($tags as $index => $tag){ if (empty($tag)) continue; @@ -1392,6 +1410,80 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return mailbox('add', 'mailbox', $mailbox_attributes); break; + case 'mta_sts': + $domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46); + $version = strtolower($_data['version']); + $mode = strtolower($_data['mode']); + $mx = explode(",", preg_replace('/\s+/', '', $_data['mx'])); + $max_age = intval($_data['max_age']); + $active = (intval($_data['active']) == 1) ? 1 : 0; + $id = date('YmdHis'); + + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => 'access_denied' + ); + return false; + } + if (empty($version) || !in_array($version, array('stsv1'))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => array('version_invalid', htmlspecialchars($domain)) + ); + return false; + } + if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => array('mode_invalid', htmlspecialchars($domain)) + ); + return false; + } + if (empty($max_age) || $max_age < 0 || $max_age > 31536000) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => array('max_age_invalid', htmlspecialchars($domain)) + ); + return false; + } + foreach ($mx as $index => $mx_domain) { + $mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46); + if (!is_valid_domain_name($mx_domain, array('allow_wildcard' => true))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => array('mx_invalid', htmlspecialchars($mx_domain)) + ); + return false; + } + } + + try { + $stmt = $pdo->prepare("INSERT INTO `mta_sts` (`id`, `domain`, `version`, `mode`, `mx`, `max_age`, `active`) + VALUES (:id, :domain, :version, :mode, :mx, :max_age, :active)"); + $stmt->execute(array( + ':id' => $id, + ':domain' => $domain, + ':version' => $version, + ':mode' => $mode, + ':mx' => implode(",", $mx), + ':max_age' => $max_age, + ':active' => $active + )); + } catch (PDOException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => $e->getMessage() + ); + return false; + } + break; case 'resource': $domain = idn_to_ascii(strtolower(trim($_data['domain'])), 0, INTL_IDNA_VARIANT_UTS46); $description = $_data['description']; @@ -1613,6 +1705,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr = array(); $attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0; $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); + $attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler']); $attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification']); $attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : strval($MAILBOX_DEFAULT_ATTRIBUTES['quarantine_category']); $attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s"; @@ -2017,15 +2110,23 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - if (empty($_data['validity'])) { + if (empty($_data['validity']) && empty($_data['permanent'])) { continue; } - $validity = round((int)time() + ($_data['validity'] * 3600)); - $stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity WHERE + if (isset($_data['permanent']) && filter_var($_data['permanent'], FILTER_VALIDATE_BOOL)) { + $permanent = 1; + $validity = 0; + } + else if (isset($_data['validity'])) { + $permanent = 0; + $validity = round((int)time() + ($_data['validity'] * 3600)); + } + $stmt = $pdo->prepare("UPDATE `spamalias` SET `validity` = :validity, `permanent` = :permanent WHERE `address` = :address"); $stmt->execute(array( ':address' => $address, - ':validity' => $validity + ':validity' => $validity, + ':permanent' => $permanent )); $_SESSION['return'][] = array( 'type' => 'success', @@ -2398,6 +2499,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { foreach ($ids as $id) { $is_now = mailbox('get', 'alias_details', $id); if (!empty($is_now)) { + $internal = (isset($_data['internal'])) ? intval($_data['internal']) : $is_now['internal']; $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $sogo_visible = (isset($_data['sogo_visible'])) ? intval($_data['sogo_visible']) : $is_now['sogo_visible']; $goto_null = (isset($_data['goto_null'])) ? intval($_data['goto_null']) : 0; @@ -2583,6 +2685,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `domain` = :domain, `goto` = :goto, `sogo_visible`= :sogo_visible, + `internal`= :internal, `active`= :active WHERE `id` = :id"); $stmt->execute(array( @@ -2592,6 +2695,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ':domain' => $domain, ':goto' => $goto, ':sogo_visible' => $sogo_visible, + ':internal' => $internal, ':active' => $active, ':id' => $is_now['id'] )); @@ -3259,6 +3363,13 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); return false; } + // save delimiter_action + if (isset($_data['tagged_mail_handler'])) { + mailbox('edit', 'delimiter_action', array( + 'username' => $username, + 'tagged_mail_handler' => $_data['tagged_mail_handler'] + )); + } // save tags foreach($tags as $index => $tag){ if (empty($tag)) continue; @@ -3604,6 +3715,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr = array(); $attr["quota"] = isset($_data['quota']) ? intval($_data['quota']) * 1048576 : 0; $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : $is_now['tags']; + $attr["tagged_mail_handler"] = (!empty($_data['tagged_mail_handler'])) ? $_data['tagged_mail_handler'] : $is_now['tagged_mail_handler']; $attr["quarantine_notification"] = (!empty($_data['quarantine_notification'])) ? $_data['quarantine_notification'] : $is_now['quarantine_notification']; $attr["quarantine_category"] = (!empty($_data['quarantine_category'])) ? $_data['quarantine_category'] : $is_now['quarantine_category']; $attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame']; @@ -3724,6 +3836,125 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return true; break; + case 'mta_sts': + if (!is_array($_data['domains'])) { + $domains = array(); + $domains[] = $_data['domains']; + } + else { + $domains = $_data['domains']; + } + + foreach ($domains as $domain) { + $domain = idn_to_ascii(strtolower(trim($domain)), 0, INTL_IDNA_VARIANT_UTS46); + + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => 'access_denied' + ); + continue; + } + + $is_now = mailbox('get', 'mta_sts', $domain); + if (!empty($is_now)) { + $version = (isset($_data['version'])) ? strtolower($_data['version']) : $is_now['version']; + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; + $active = ($active == 1) ? 1 : 0; + $mode = (isset($_data['mode'])) ? strtolower($_data['mode']) : $is_now['mode']; + $mx = (isset($_data['mx'])) ? explode(",", preg_replace('/\s+/', '', $_data['mx'])) : $is_now['mx']; + $max_age = (isset($_data['max_age'])) ? intval($_data['max_age']) : $is_now['max_age']; + + // Update ID if neccesary + if ($version != strtolower($is_now['version']) || + $mode != strtolower($is_now['mode']) || + $mx != $is_now['mx'] || + $max_age != $is_now['max_age']) { + $id = date('YmdHis'); + } else { + $id = $is_now['id']; + } + + } else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'access_denied' + ); + continue; + } + + if (empty($version) || !in_array($version, array('stsv1'))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => array('version_invalid', htmlspecialchars($version)) + ); + continue; + } + if (empty($mode) || !in_array($mode, array('enforce', 'testing', 'none'))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => array('mode_invalid', htmlspecialchars($domain)) + ); + continue; + } + if (empty($max_age) || $max_age < 0 || $max_age > 31557600) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => array('max_age_invalid', htmlspecialchars($domain)) + ); + continue; + } + foreach ($mx as $index => $mx_domain) { + $mx_domain = idn_to_ascii(strtolower(trim($mx_domain)), 0, INTL_IDNA_VARIANT_UTS46); + $invalid_mx = false; + if (!is_valid_domain_name($mx_domain, array('allow_wildcard' => true))) { + $invalid_mx = $mx_domain; + break; + } + } + if ($invalid_mx) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => array('mx_invalid', htmlspecialchars($invalid_mx)) + ); + continue; + } + + try { + $stmt = $pdo->prepare("UPDATE `mta_sts` SET `id` = :id, `version` = :version, `mode` = :mode, `mx` = :mx, `max_age` = :max_age, `active` = :active WHERE `domain` = :domain"); + $stmt->execute(array( + ':id' => $id, + ':domain' => $domain, + ':version' => $version, + ':mode' => $mode, + ':mx' => implode(",", $mx), + ':max_age' => $max_age, + ':active' => $active + )); + } catch (PDOException $e) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data), + 'msg' => $e->getMessage() + ); + continue; + } + + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data, $_attr), + 'msg' => array('object_modified', $domain) + ); + } + + return true; + break; case 'resource': if (!is_array($_data['name'])) { $names = array(); @@ -4368,10 +4599,12 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `description`, `validity`, `created`, - `modified` + `modified`, + `permanent` FROM `spamalias` WHERE `goto` = :username - AND `validity` >= :unixnow"); + AND (`validity` >= :unixnow + OR `permanent` != 0)"); $stmt->execute(array(':username' => $_data, ':unixnow' => time())); $tladata = $stmt->fetchAll(PDO::FETCH_ASSOC); return $tladata; @@ -4490,6 +4723,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { `address`, `public_comment`, `private_comment`, + `internal`, `active`, `sogo_visible`, `created`, @@ -4520,6 +4754,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $aliasdata['goto'] = $row['goto']; $aliasdata['address'] = $row['address']; (!filter_var($aliasdata['address'], FILTER_VALIDATE_EMAIL)) ? $aliasdata['is_catch_all'] = 1 : $aliasdata['is_catch_all'] = 0; + $aliasdata['internal'] = $row['internal']; $aliasdata['active'] = $row['active']; $aliasdata['active_int'] = $row['active']; $aliasdata['sogo_visible'] = $row['sogo_visible']; @@ -4944,7 +5179,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt = $pdo->prepare("SELECT COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE (`kind` = '' OR `kind` = NULL) AND `domain` = :domain AND `username` != :username"); $stmt->execute(array(':domain' => $row['domain'], ':username' => $_data)); $MailboxUsage = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND `validity` >= :unixnow"); + $stmt = $pdo->prepare("SELECT IFNULL(COUNT(`address`), 0) AS `sa_count` FROM `spamalias` WHERE `goto` = :address AND (`validity` >= :unixnow OR `permanent` != 0)"); $stmt->execute(array(':address' => $_data, ':unixnow' => time())); $SpamaliasUsage = $stmt->fetch(PDO::FETCH_ASSOC); $mailboxdata['max_new_quota'] = ($DomainQuota['quota'] * 1048576) - $MailboxUsage['in_use']; @@ -5012,6 +5247,20 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { return $rows; } break; + case 'mta_sts': + $stmt = $pdo->prepare("SELECT * FROM `mta_sts` WHERE `domain` = :domain"); + $stmt->execute(array( + ':domain' => $_data, + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)){ + return []; + } + $row['mx'] = explode(',', $row['mx']); + $row['version'] = strtoupper(substr($row['version'], 0, 3)) . substr($row['version'], 3); + + return $row; + break; case 'resource_details': $resourcedata = array(); if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) { @@ -5397,6 +5646,10 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $stmt->execute(array( ':domain' => $domain, )); + $stmt = $pdo->prepare("DELETE FROM `mta_sts` WHERE `domain` = :domain"); + $stmt->execute(array( + ':domain' => $domain, + )); $stmt = $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);"); $stmt = $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);"); try { diff --git a/mailcow/data/web/inc/functions.quarantine.inc.php b/mailcow/data/web/inc/functions.quarantine.inc.php index f4f49de..5df86ef 100644 --- a/mailcow/data/web/inc/functions.quarantine.inc.php +++ b/mailcow/data/web/inc/functions.quarantine.inc.php @@ -22,7 +22,7 @@ function quarantine($_action, $_data = null) { return false; } $stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt` - WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash + WHERE `qhash` = :hash AND user_acl.quarantine = 1 AND rcpt IN (SELECT username FROM mailbox)'); $stmt->execute(array(':hash' => $hash)); @@ -65,7 +65,7 @@ function quarantine($_action, $_data = null) { return false; } $stmt = $pdo->prepare('SELECT `id` FROM `quarantine` LEFT OUTER JOIN `user_acl` ON `user_acl`.`username` = `rcpt` - WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash + WHERE `qhash` = :hash AND `user_acl`.`quarantine` = 1 AND `username` IN (SELECT `username` FROM `mailbox`)'); $stmt->execute(array(':hash' => $hash)); @@ -169,7 +169,7 @@ function quarantine($_action, $_data = null) { } } elseif ($release_format == 'raw') { - $detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $detail_row['msg']); + $detail_row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $detail_row['msg']); $postfix_talk = array( array('220', 'HELO quarantine' . chr(10)), array('250', 'MAIL FROM: ' . $sender . chr(10)), @@ -464,7 +464,7 @@ function quarantine($_action, $_data = null) { } } elseif ($release_format == 'raw') { - $row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/', 'X-Pre-Release-Spam-Flag $1', $row['msg']); + $row['msg'] = preg_replace('/^X-Spam-Flag: (.*)/m', 'X-Pre-Release-Spam-Flag: $1', $row['msg']); $postfix_talk = array( array('220', 'HELO quarantine' . chr(10)), array('250', 'MAIL FROM: ' . $sender . chr(10)), @@ -833,7 +833,7 @@ function quarantine($_action, $_data = null) { ))); return false; } - $stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE SHA2(CONCAT(`id`, `qid`), 256) = :hash'); + $stmt = $pdo->prepare('SELECT * FROM `quarantine` WHERE `qhash` = :hash'); $stmt->execute(array(':hash' => $hash)); return $stmt->fetch(PDO::FETCH_ASSOC); break; diff --git a/mailcow/data/web/inc/header.inc.php b/mailcow/data/web/inc/header.inc.php index 9ab2ad1..d2ce6f3 100644 --- a/mailcow/data/web/inc/header.inc.php +++ b/mailcow/data/web/inc/header.inc.php @@ -62,7 +62,11 @@ if ($app_links_processed){ } } - +// Workaround to get text with
straight to twig. +// Using "nl2br" doesn't work with Twig as it would escape everything by default. +if (isset($UI_TEXTS["ui_footer"])) { + $UI_TEXTS["ui_footer"] = nl2br($UI_TEXTS["ui_footer"]); +} $globalVariables = [ 'mailcow_hostname' => getenv('MAILCOW_HOSTNAME'), diff --git a/mailcow/data/web/inc/init_db.inc.php b/mailcow/data/web/inc/init_db.inc.php index 767a002..ffaf120 100644 --- a/mailcow/data/web/inc/init_db.inc.php +++ b/mailcow/data/web/inc/init_db.inc.php @@ -4,7 +4,7 @@ function init_db_schema() try { global $pdo; - $db_version = "27012025_1555"; + $db_version = "10312025_0525"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -184,6 +184,7 @@ function init_db_schema() "private_comment" => "TEXT", "public_comment" => "TEXT", "sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'", + "internal" => "TINYINT(1) NOT NULL DEFAULT '0'", "active" => "TINYINT(1) NOT NULL DEFAULT '1'" ), "keys" => array( @@ -345,10 +346,14 @@ function init_db_schema() "notified" => "TINYINT(1) NOT NULL DEFAULT '0'", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", + "qhash" => "VARCHAR(64)", ), "keys" => array( "primary" => array( "" => array("id") + ), + "key" => array( + "qhash" => array("qhash") ) ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" @@ -471,6 +476,23 @@ function init_db_schema() ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), + "mta_sts" => array( + "cols" => array( + "id" => "BIGINT NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "version" => "VARCHAR(255) NOT NULL", + "mode" => "VARCHAR(255) NOT NULL", + "mx" => "VARCHAR(255) NOT NULL", + "max_age" => "VARCHAR(255) NOT NULL", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "user_acl" => array( "cols" => array( "username" => "VARCHAR(255) NOT NULL", @@ -532,7 +554,8 @@ function init_db_schema() "description" => "TEXT NOT NULL", "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "validity" => "INT(11)" + "validity" => "INT(11)", + "permanent" => "TINYINT(1) NOT NULL DEFAULT '0'" ), "keys" => array( "primary" => array( @@ -1315,6 +1338,14 @@ function init_db_schema() $pdo->query($create); } + // Clear old app_passwd log entries + $pdo->exec("DELETE FROM logs + WHERE role != 'unauthenticated' + AND JSON_EXTRACT(`call`, '$[0]') = 'app_passwd' + AND JSON_EXTRACT(`call`, '$[1]') = 'edit' + AND (JSON_CONTAINS_PATH(`call`, 'one', '$[2].password') + OR JSON_CONTAINS_PATH(`call`, 'one', '$[2].password2'));"); + // Mitigate imapsync argument injection issue $pdo->query("UPDATE `imapsync` SET `custom_params` = '' WHERE `custom_params` LIKE '%pipemess%' @@ -1482,6 +1513,10 @@ function init_db_schema() 'msg' => 'db_init_complete' ); } + + // fill quarantine.qhash + $pdo->query("UPDATE `quarantine` SET `qhash` = SHA2(CONCAT(`id`, `qid`), 256) WHERE ISNULL(`qhash`)"); + } catch (PDOException $e) { if (php_sapi_name() == "cli") { echo "DB initialization failed: " . print_r($e, true) . PHP_EOL; diff --git a/mailcow/data/web/inc/sessions.inc.php b/mailcow/data/web/inc/sessions.inc.php index bbc08cf..8f3192d 100644 --- a/mailcow/data/web/inc/sessions.inc.php +++ b/mailcow/data/web/inc/sessions.inc.php @@ -1,7 +1,9 @@ 'Català (Catalan)', + 'bg-bg' => 'Български (Bulgarian)', 'cs-cz' => 'Čeština (Czech)', - 'da-dk' => 'Danish (Dansk)', + 'da-dk' => 'Dansk (Danish)', 'de-de' => 'Deutsch (German)', 'en-gb' => 'English', 'es-es' => 'Español (Spanish)', @@ -109,6 +110,7 @@ $AVAILABLE_LANGUAGES = array( 'sv-se' => 'Svenska (Swedish)', 'tr-tr' => 'Türkçe (Turkish)', 'uk-ua' => 'Українська (Ukrainian)', + 'vi-vn' => 'Tiếng Việt (Vietnamese)', 'zh-cn' => '简体中文 (Simplified Chinese)', 'zh-tw' => '繁體中文 (Traditional Chinese)', ); @@ -152,6 +154,13 @@ $LOG_PAGINATION_SIZE = 50; // Session lifetime in seconds $SESSION_LIFETIME = 10800; +// Session SameSite Policy +// Use "None", "Lax" or "Strict" +$SESSION_SAMESITE_POLICY = "Lax"; + +// Name of the session cookie +$SESSION_NAME = "MCSESSID"; + // Label for OTP devices $OTP_LABEL = "mailcow UI"; @@ -185,6 +194,12 @@ $MAILBOX_DEFAULT_ATTRIBUTES['force_pw_update'] = 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; +// How to handle tagged emails +// none - No special handling +// subfolder - Create subfolder under INBOX (e.g. "INBOX/Facebook") +// subject - Add tag to subject (e.g. "[Facebook] Subject") +$MAILBOX_DEFAULT_ATTRIBUTES['tagged_mail_handler'] = "none"; + // Send notification when quarantine is not empty (never, hourly, daily, weekly) $MAILBOX_DEFAULT_ATTRIBUTES['quarantine_notification'] = 'hourly'; @@ -237,12 +252,12 @@ $FIDO2_FORMATS = array('apple', 'android-key', 'android-safetynet', 'fido-u2f', // Set visible Rspamd maps in mailcow UI, do not change unless you know what you are doing $RSPAMD_MAPS = array( 'regex' => array( - 'Header-From: Blacklist' => 'global_mime_from_blacklist.map', - 'Header-From: Whitelist' => 'global_mime_from_whitelist.map', - 'Envelope Sender Blacklist' => 'global_smtp_from_blacklist.map', - 'Envelope Sender Whitelist' => 'global_smtp_from_whitelist.map', - 'Recipient Blacklist' => 'global_rcpt_blacklist.map', - 'Recipient Whitelist' => 'global_rcpt_whitelist.map', + 'Header-From: Denylist' => 'global_mime_from_blacklist.map', + 'Header-From: Allowlist' => 'global_mime_from_whitelist.map', + 'Envelope Sender Denylist' => 'global_smtp_from_blacklist.map', + 'Envelope Sender Allowlist' => 'global_smtp_from_whitelist.map', + 'Recipient Denylist' => 'global_rcpt_blacklist.map', + 'Recipient Allowlist' => 'global_rcpt_whitelist.map', 'Fishy TLDS (only fired in combination with bad words)' => 'fishy_tlds.map', 'Bad Words (only fired in combination with fishy TLDs)' => 'bad_words.map', 'Bad Words DE (only fired in combination with fishy TLDs)' => 'bad_words_de.map', @@ -256,57 +271,57 @@ $RSPAMD_MAPS = array( $IMAPSYNC_OPTIONS = array( 'whitelist' => array( - 'abort', - 'authmd51', - 'authmd52', + 'abort', + 'authmd51', + 'authmd52', 'authmech1', 'authmech2', - 'authuser1', - 'authuser2', - 'debug', - 'debugcontent', - 'debugcrossduplicates', - 'debugflags', - 'debugfolders', - 'debugimap', - 'debugimap1', - 'debugimap2', - 'debugmemory', - 'debugssl', + 'authuser1', + 'authuser2', + 'debug', + 'debugcontent', + 'debugcrossduplicates', + 'debugflags', + 'debugfolders', + 'debugimap', + 'debugimap1', + 'debugimap2', + 'debugmemory', + 'debugssl', 'delete1emptyfolders', - 'delete2folders', - 'disarmreadreceipts', + 'delete2folders', + 'disarmreadreceipts', 'domain1', 'domain2', - 'domino1', - 'domino2', + 'domino1', + 'domino2', 'dry', 'errorsmax', - 'exchange1', - 'exchange2', + 'exchange1', + 'exchange2', 'exitwhenover', 'expunge1', - 'f1f2', - 'filterbuggyflags', + 'f1f2', + 'filterbuggyflags', 'folder', 'folderfirst', 'folderlast', 'folderrec', - 'gmail1', - 'gmail2', - 'idatefromheader', + 'gmail1', + 'gmail2', + 'idatefromheader', 'include', 'inet4', 'inet6', - 'justconnect', - 'justfolders', - 'justfoldersizes', - 'justlogin', - 'keepalive1', - 'keepalive2', + 'justconnect', + 'justfolders', + 'justfoldersizes', + 'justlogin', + 'keepalive1', + 'keepalive2', 'log', 'logdir', - 'logfile', + 'logfile', 'maxbytesafter', 'maxlinelength', 'maxmessagespersecond', @@ -314,62 +329,62 @@ $IMAPSYNC_OPTIONS = array( 'maxsleep', 'minage', 'minsize', - 'noabletosearch', - 'noabletosearch1', - 'noabletosearch2', - 'noexpunge1', - 'noexpunge2', + 'noabletosearch', + 'noabletosearch1', + 'noabletosearch2', + 'noexpunge1', + 'noexpunge2', 'nofoldersizesatend', - 'noid', - 'nolog', - 'nomixfolders', - 'noresyncflags', - 'nossl1', - 'nossl2', - 'nosyncacls', - 'notls1', - 'notls2', - 'nouidexpunge2', - 'nousecache', + 'noid', + 'nolog', + 'nomixfolders', + 'noresyncflags', + 'nossl1', + 'nossl2', + 'nosyncacls', + 'notls1', + 'notls2', + 'nouidexpunge2', + 'nousecache', 'oauthaccesstoken1', 'oauthaccesstoken2', 'oauthdirect1', 'oauthdirect2', - 'office1', - 'office2', - 'pidfile', - 'pidfilelocking', + 'office1', + 'office2', + 'pidfile', + 'pidfilelocking', 'prefix1', 'prefix2', - 'proxyauth1', - 'proxyauth2', - 'resyncflags', - 'resynclabels', - 'search', + 'proxyauth1', + 'proxyauth2', + 'resyncflags', + 'resynclabels', + 'search', 'search1', - 'search2', + 'search2', 'sep1', 'sep2', 'showpasswords', 'skipemptyfolders', - 'ssl2', + 'ssl2', 'sslargs1', - 'sslargs2', + 'sslargs2', 'subfolder1', - 'subscribe', + 'subscribe', 'subscribed', 'syncacls', 'syncduplicates', 'syncinternaldates', - 'synclabels', - 'tests', - 'testslive', - 'testslive6', - 'tls2', - 'truncmess', - 'usecache', - 'useheader', - 'useuid' + 'synclabels', + 'tests', + 'testslive', + 'testslive6', + 'tls2', + 'truncmess', + 'usecache', + 'useheader', + 'useuid' ), 'blacklist' => array( 'skipmess', diff --git a/mailcow/data/web/index.php b/mailcow/data/web/index.php index 1e91cb7..d4fa46e 100644 --- a/mailcow/data/web/index.php +++ b/mailcow/data/web/index.php @@ -11,8 +11,8 @@ if (isset($_SESSION['mailcow_cc_role']) && isset($_SESSION['oauth2_request'])) { 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) { - header("Location: /SOGo/so/{$_SESSION['mailcow_cc_username']}"); + if (intval($user_details['attributes']['sogo_access']) == 1 && !$is_dual && getenv('SKIP_SOGO') != "y") { + header("Location: /SOGo/so/"); } else { header("Location: /user"); } @@ -33,16 +33,18 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING']; $has_iam_sso = false; if ($iam_provider){ - $has_iam_sso = identity_provider("get-redirect") ? true : false; + $iam_redirect_url = identity_provider("get-redirect"); + $has_iam_sso = $iam_redirect_url ? true : false; } - +$custom_login = customize('get', 'custom_login'); $template = 'user_index.twig'; $template_data = [ 'oauth2_request' => @$_SESSION['oauth2_request'], 'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'), 'login_delay' => @$_SESSION['ldelay'], - 'has_iam_sso' => $has_iam_sso + 'has_iam_sso' => $has_iam_sso, + 'custom_login' => $custom_login, ]; $js_minifier->add('/web/js/site/index.js'); diff --git a/mailcow/data/web/js/build/013-mailcow.js b/mailcow/data/web/js/build/013-mailcow.js index f090983..d897f23 100644 --- a/mailcow/data/web/js/build/013-mailcow.js +++ b/mailcow/data/web/js/build/013-mailcow.js @@ -22,8 +22,8 @@ $(document).ready(function() { $.notify({message: msg},{z_index: 20000, delay: auto_hide, type: type,placement: {from: "bottom",align: "right"},animate: {enter: 'animated fadeInUp',exit: 'animated fadeOutDown'}}); } - $(".generate_password").click(async function( event ) { - try { + $(".generate_password").click(async function( event ) { + try { var password_policy = await window.fetch("/api/v1/get/passwordpolicy", { method:'GET', cache:'no-cache' }); var password_policy = await password_policy.json(); random_passwd_length = password_policy.length; @@ -48,7 +48,11 @@ $(document).ready(function() { }) } $(".rot-enc").html(function(){ - return str_rot13($(this).html()) + footer_html = $(this).html(); + footer_html = footer_html.replace(/</g, '<').replace(/>/g, '>') + .replace(/&/g, '&').replace(/&nzc;/g, '&') + .replace(/"/g, '"').replace(/'/g, "'"); + return str_rot13(footer_html) }); // https://stackoverflow.com/questions/4399005/implementing-jquerys-shake-effect-with-animate function shake(div,interval,distance,times) { @@ -125,7 +129,7 @@ $(document).ready(function() { } }); })(); - + // responsive tabs, scroll to opened tab $(document).on("shown.bs.collapse shown.bs.tab", function (e) { var target = $(e.target); @@ -409,4 +413,4 @@ function copyToClipboard(id) { // only works with https connections navigator.clipboard.writeText(copyText.value); mailcow_alert_box(lang.copy_to_clipboard, "success"); -} \ No newline at end of file +} diff --git a/mailcow/data/web/js/site/admin.js b/mailcow/data/web/js/site/admin.js index 847c25a..009a27f 100644 --- a/mailcow/data/web/js/site/admin.js +++ b/mailcow/data/web/js/site/admin.js @@ -51,7 +51,7 @@ jQuery(function($){ $('.submit_rspamd_regex').attr({"disabled": true}); }); $("#show_rspamd_global_filters").click(function() { - $.get("inc/ajax/show_rspamd_global_filters.php"); + $.get("/inc/ajax/show_rspamd_global_filters.php"); $("#confirm_show_rspamd_global_filters").hide(); $("#rspamd_global_filters").removeClass("d-none"); }); @@ -558,7 +558,7 @@ jQuery(function($){ } else if (table == 'oauth2clientstable') { $.each(data, function (i, item) { item.action = ''; item.scope = "profile"; @@ -573,7 +573,7 @@ jQuery(function($){ item.action = ''; }); } else if (table == 'adminstable') { @@ -655,7 +655,7 @@ jQuery(function($){ $(this).html(' '); $.ajax({ type: 'GET', - url: 'inc/ajax/relay_check.php', + url: '/inc/ajax/relay_check.php', dataType: 'text', data: $('#test_relayhost_form').serialize(), complete: function (data) { @@ -715,7 +715,6 @@ jQuery(function($){ $('.app_hide').off('change'); $('.app_hide').on('change', function (e) { var value = $(this).is(':checked') ? '1' : '0'; - console.log(value) $(this).parent().children(':first-child').val(value); }) } @@ -789,6 +788,18 @@ jQuery(function($){ $('.iam_ldap_rolemap_del').click(async function(e){ deleteAttributeMappingRow(this, e); }); + $('.iam_redirect_add_keycloak').click(async function(e){ + addRedirectUrlRow('#iam_keycloak_redirect_list', '.iam_keycloak_redirect_del', e); + }); + $('.iam_redirect_add_generic').click(async function(e){ + addRedirectUrlRow('#iam_generic_redirect_list', '.iam_generic_redirect_del', e); + }); + $('.iam_keycloak_redirect_del').click(async function(e){ + deleteRedirectUrlRow(this, e); + }); + $('.iam_generic_redirect_del').click(async function(e){ + deleteRedirectUrlRow(this, e); + }); // selecting identity provider $('#iam_provider').on('change', function(){ // toggle password fields @@ -833,4 +844,22 @@ jQuery(function($){ if ($(elem).parent().parent().parent().parent().children().length > 1) $(elem).parent().parent().parent().remove(); } + function addRedirectUrlRow(list_id, del_class, e) { + e.preventDefault(); + + var parent = $(list_id) + $(parent).children().last().clone().appendTo(parent); + var newChild = $(parent).children().last(); + $(newChild).find('input').val(''); + + $(del_class).off('click'); + $(del_class).click(async function(e){ + deleteRedirectUrlRow(this, e); + }); + } + function deleteRedirectUrlRow(elem, e) { + e.preventDefault(); + if ($(elem).parent().parent().parent().parent().children().length > 2) + $(elem).parent().parent().parent().remove(); + } }); diff --git a/mailcow/data/web/js/site/dashboard.js b/mailcow/data/web/js/site/dashboard.js index 182a09a..760e878 100644 --- a/mailcow/data/web/js/site/dashboard.js +++ b/mailcow/data/web/js/site/dashboard.js @@ -47,8 +47,6 @@ $(document).ready(function() { window.fetch("/api/v1/get/status/host/ip", { method:'GET', cache:'no-cache' }).then(function(response) { return response.json(); }).then(function(data) { - console.log(data); - // display host ips if (data.ipv4) $("#host_ipv4").text(data.ipv4); @@ -1007,7 +1005,7 @@ jQuery(function($){ "data-order": cellData.sortBy, "data-sort": cellData.sortBy }); - }, + }, render: function (data) { return data.value; } @@ -1032,7 +1030,7 @@ jQuery(function($){ "data-order": cellData.sortBy, "data-sort": cellData.sortBy }); - }, + }, render: function (data) { return data.value; } @@ -1348,8 +1346,6 @@ function update_stats(timeout=5){ window.fetch("/api/v1/get/status/host", {method:'GET',cache:'no-cache'}).then(function(response) { return response.json(); }).then(function(data) { - console.log(data); - if (data){ // display table data $("#host_date").text(data.system_time); @@ -1399,8 +1395,6 @@ function update_container_stats(timeout=5){ var diskIOCtx = Chart.getChart(container + "_DiskIOChart"); var netIOCtx = Chart.getChart(container + "_NetIOChart"); - console.log(container); - console.log(data); prev_stats = null; if (data.length >= 2){ prev_stats = data[data.length -2]; diff --git a/mailcow/data/web/js/site/edit.js b/mailcow/data/web/js/site/edit.js index f9fe707..308ec8c 100644 --- a/mailcow/data/web/js/site/edit.js +++ b/mailcow/data/web/js/site/edit.js @@ -66,7 +66,6 @@ $(document).ready(function() { // load tags if ($('#tags').length){ var tagsEl = $('#tags').parent().find('.tag-values')[0]; - console.log($(tagsEl).val()) var tags = JSON.parse($(tagsEl).val()); $(tagsEl).val(""); diff --git a/mailcow/data/web/js/site/mailbox.js b/mailcow/data/web/js/site/mailbox.js index 28e6fd2..df61f87 100644 --- a/mailcow/data/web/js/site/mailbox.js +++ b/mailcow/data/web/js/site/mailbox.js @@ -269,6 +269,24 @@ $(document).ready(function() { function setMailboxTemplateData(template){ $("#addInputQuota").val(template.quota / 1048576); + if (template.tagged_mail_handler === "subfolder"){ + $('#tagged_mail_handler_subfolder').prop('checked', true); + $('#tagged_mail_handler_subject').prop('checked', false); + $('#tagged_mail_handler_none').prop('checked', false); + } else if(template.tagged_mail_handler === "subject"){ + $('#tagged_mail_handler_subfolder').prop('checked', false); + $('#tagged_mail_handler_subject').prop('checked', true); + $('#tagged_mail_handler_none').prop('checked', false); + } else if(template.tagged_mail_handler === "none"){ + $('#tagged_mail_handler_subfolder').prop('checked', false); + $('#tagged_mail_handler_subject').prop('checked', false); + $('#tagged_mail_handler_none').prop('checked', true); + } else { + $('#tagged_mail_handler_subfolder').prop('checked', false); + $('#tagged_mail_handler_subject').prop('checked', false); + $('#tagged_mail_handler_none').prop('checked', true); + } + if (template.quarantine_notification === "never"){ $('#quarantine_notification_never').prop('checked', true); $('#quarantine_notification_hourly').prop('checked', false); @@ -1931,11 +1949,6 @@ jQuery(function($){ defaultContent: '', responsivePriority: 5, }, - { - title: lang.bcc_destinations, - data: 'bcc_dest', - defaultContent: '' - }, { title: lang.sogo_visible, data: 'sogo_visible', @@ -1954,6 +1967,15 @@ jQuery(function($){ data: 'private_comment', defaultContent: '' }, + { + title: lang.internal, + data: 'internal', + defaultContent: '', + responsivePriority: 6, + render: function (data, type) { + return 1==data?'1':0==data&&'0'; + } + }, { title: lang.active, data: 'active', diff --git a/mailcow/data/web/js/site/user.js b/mailcow/data/web/js/site/user.js index 678e23b..5eecf20 100644 --- a/mailcow/data/web/js/site/user.js +++ b/mailcow/data/web/js/site/user.js @@ -97,18 +97,17 @@ jQuery(function($){ var datetime = new Date(item.datetime.replace(/-/g, "/")); var local_datetime = datetime.toLocaleDateString(undefined, {year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit"}); 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 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 ip_data = real_rip + ip_location + app_password; $(".last-sasl-login").append(`
  • -
    ` + real_rip + `
    - ` + service + ` ` + local_datetime + ` +
    ` + ip_location + real_rip + `
    + ` + service + ` ` + local_datetime + `` + app_password + `
    - ` + ip_location + `
  • `); }) @@ -169,7 +168,6 @@ jQuery(function($){ type: "GET", url: "/api/v1/get/time_limited_aliases", dataSrc: function(data){ - console.log(data); $.each(data, function (i, item) { if (acl_data.spam_alias === 1) { item.action = '
    ' + @@ -177,6 +175,10 @@ jQuery(function($){ '
    '; item.chkbox = ''; item.address = escapeHtml(item.address); + item.validity = { + value: item.validity, + permanent: item.permanent + }; } else { item.chkbox = ''; @@ -220,9 +222,21 @@ jQuery(function($){ title: lang.alias_valid_until, data: 'validity', defaultContent: '', - createdCell: function(td, cellData) { - createSortableDate(td, cellData) - } + render: function (data, type) { + var date = new Date(data.value ? data.value * 1000 : 0); + switch (type) { + case "sort": + if (data.permanent) { + return 0; + } + return date.getTime(); + default: + if (data.permanent) { + return lang.forever; + } + return date.toLocaleDateString(LOCALE, DATETIME_FORMAT); + } + }, }, { title: lang.created_on, @@ -262,7 +276,6 @@ jQuery(function($){ type: "GET", url: '/api/v1/get/syncjobs/' + encodeURIComponent(mailcow_cc_username) + '/no_log', dataSrc: function(data){ - console.log(data); $.each(data, function (i, item) { item.user1 = escapeHtml(item.user1); item.log = '' + lang.open_logs + '' @@ -418,7 +431,6 @@ jQuery(function($){ type: "GET", url: '/api/v1/get/app-passwd/all', dataSrc: function(data){ - console.log(data); $.each(data, function (i, item) { item.name = escapeHtml(item.name) item.protocols = [] @@ -514,7 +526,6 @@ jQuery(function($){ type: "GET", url: '/api/v1/get/policy_wl_mailbox', dataSrc: function(data){ - console.log(data); $.each(data, function (i, item) { if (validateEmail(item.object)) { item.chkbox = ''; @@ -585,7 +596,6 @@ jQuery(function($){ type: "GET", url: '/api/v1/get/policy_bl_mailbox', dataSrc: function(data){ - console.log(data); $.each(data, function (i, item) { if (validateEmail(item.object)) { item.chkbox = ''; diff --git a/mailcow/data/web/json_api.php b/mailcow/data/web/json_api.php index f2a222b..565246e 100644 --- a/mailcow/data/web/json_api.php +++ b/mailcow/data/web/json_api.php @@ -324,6 +324,9 @@ if (isset($_GET['query'])) { case "app-passwd": process_add_return(app_passwd('add', $attr)); break; + case "mta-sts": + process_add_return(mailbox('add', 'mta_sts', $attr)); + break; // return no route found if no case is matched default: http_response_code(404); @@ -1976,6 +1979,9 @@ if (isset($_GET['query'])) { case "ip_check": process_edit_return(customize('edit', 'ip_check', $attr)); break; + case "custom_login": + process_edit_return(customize('edit', 'custom_login', $attr)); + break; case "self": if ($_SESSION['mailcow_cc_role'] == "domainadmin") { process_edit_return(domain_admin('edit', $attr)); @@ -1986,6 +1992,7 @@ if (isset($_GET['query'])) { break; case "cors": process_edit_return(cors('edit', $attr)); + break; case "identity-provider": process_edit_return(identity_provider('edit', $attr)); break; @@ -1998,6 +2005,9 @@ if (isset($_GET['query'])) { case "reset-password-notification": process_edit_return(reset_password('edit_notification', $attr)); break; + case "mta-sts": + process_edit_return(mailbox('edit', 'mta_sts', array_merge(array('domains' => $items), $attr))); + break; // return no route found if no case is matched default: http_response_code(404); diff --git a/mailcow/data/web/lang/lang.bg-bg.json b/mailcow/data/web/lang/lang.bg-bg.json new file mode 100644 index 0000000..d17c4dd --- /dev/null +++ b/mailcow/data/web/lang/lang.bg-bg.json @@ -0,0 +1,1391 @@ +{ + "acl": { + "alias_domains": "Добавяне на домейни за псевдоними", + "app_passwds": "Управление на пароли за приложения", + "bcc_maps": "BCC карти", + "delimiter_action": "Действие на разделител", + "domain_desc": "Промяна на описанието на домейна", + "domain_relayhost": "Промяна на релеен хост за домейн", + "eas_reset": "Нулиране на устройствата с ActiveSync", + "extend_sender_acl": "Разрешаване на разширяване на ACL на изпращащите от външни адреси", + "filters": "Филтри", + "login_as": "Вход като потребител на пощенска кутия", + "mailbox_relayhost": "Промяна на релеен хост за пощенска кутия", + "prohibited": "Забранено от ACL", + "protocol_access": "Промяна на достъпа до протоколи", + "pushover": "Pushover", + "pw_reset": "Разрешаване на нулиране на паролата на потребител на mailcow", + "quarantine": "Действия с карантина", + "quarantine_attachments": "Прикачени файлове в карантина", + "quarantine_category": "Промяна на категорията на уведомленията за карантина", + "quarantine_notification": "Промяна на уведомленията за карантина", + "ratelimit": "Ограничение на скоростта", + "recipient_maps": "Карти на получатели", + "smtp_ip_access": "Промяна на разрешените хостове за SMTP", + "sogo_access": "Разрешаване на управление на достъпа до SOGo", + "sogo_profile_reset": "Нулиране на профила на SOGo", + "spam_alias": "Временни псевдоними", + "spam_policy": "Черен/Бял списък", + "spam_score": "Резултат за спам", + "syncjobs": "Синхронизиращи задачи", + "tls_policy": "Политика за TLS", + "unlimited_quota": "Неограничена квота за пощенски кутии" + }, + "add": { + "activate_filter_warn": "Всички други филтри ще бъдат деактивирани, когато активното е отбелязано.", + "active": "Активен", + "add": "Добавяне", + "add_domain_only": "Добавяне само на домейн", + "add_domain_restart": "Добавяне на домейн и рестартиране на SOGo", + "alias_address": "Адрес/и за псевдоним", + "alias_address_info": "Пълни имейл адреси или @example.com, за да хванете всички съобщения за домейн (разделени с запетая). само домейни на mailcow.", + "alias_domain": "Домейн за псевдоним", + "alias_domain_info": "Валидни имена на домейни (разделени с запетая).", + "app_name": "Име на приложението", + "app_password": "Добавяне на парола за приложение", + "app_passwd_protocols": "Разрешени протоколи за паролата на приложението", + "automap": "Опит за автоматично картографиране на папки (\"Изпратени\", \"Изпратени\" => \"Изпратени\" и т.н.)", + "backup_mx_options": "Опции за реле", + "bcc_dest_format": "BCC дестинацията трябва да бъде един валиден имейл адрес.
    Ако имате нужда да изпратите копие до множество адреси, създайте псевдоним и го използвайте тук.", + "comment_info": "Частен коментар не е видим за потребителя, докато публичен коментар се показва като подсказка при преминаване с мишката върху него в прегледа на потребителя", + "custom_params": "Персонализирани параметри", + "custom_params_hint": "Дясно: --param=xy, грешно: --param xy", + "delete1": "Изтриване от източника след завършване", + "delete2": "Изтриване на съобщенията в дестинацията, които не са в източника", + "delete2duplicates": "Изтриване на дубликати в дестинацията", + "description": "Описание", + "destination": "Дестинация", + "disable_login": "Забраняване на вход (входящите съобщения все още се приемат)", + "domain": "Домейн", + "domain_matches_hostname": "Домейнът %s съвпада с името на хоста", + "domain_quota_m": "Обща квота на домейна (MiB)", + "dry": "Симулиране на синхронизация", + "enc_method": "Метод на криптиране", + "exclude": "Изключване на обекти (regex)", + "full_name": "Пълно име", + "gal": "Глобален адресен списък", + "gal_info": "Глобалният адресен списък съдържа всички обекти на домейна и не може да бъде редактиран от нито един потребител. Липсва информация за заетост/свободно време в SOGo, ако е деактивирано! Рестартирайте SOGo, за да приложите промените.", + "generate": "генериране", + "goto_ham": "Учен като не е спам", + "goto_null": "Тихо изхвърляне на имейла", + "goto_spam": "Учен като спам", + "hostname": "Хост", + "inactive": "Неактивен", + "kind": "Вид", + "mailbox_quota_def": "Квота по подразбиране за пощенска кутия", + "mailbox_quota_m": "Макс. квота за пощенска кутия (MiB)", + "mailbox_username": "Потребителско име (лява част на имейл адрес)", + "max_aliases": "Макс. възможни псевдоними", + "max_mailboxes": "Макс. възможни пощенски кутии", + "mins_interval": "Интервал за проверка (минути)", + "multiple_bookings": "Множествени резервации", + "nexthop": "Следващ хоп", + "password": "Парола", + "password_repeat": "Потвърждаване на паролата (повторете)", + "port": "Порт", + "post_domain_add": "След добавянето на нов домейн, контейнерът на SOGo, \"sogo-mailcow\", трябва да бъде рестартиран!

    Освен това, конфигурацията на DNS на домейна трябва да бъде прегледана. След като конфигурацията на DNS бъде одобрена, рестартирайте \"acme-mailcow\", за да генерирате автоматично сертификати за вашия нов домейн (autoconfig.<domain>, autodiscover.<domain>).
    Тази стъпка е опционална и ще бъде повторена всеки 24 часа.", + "private_comment": "Частен коментар", + "public_comment": "Публичен коментар", + "quota_mb": "Квота (MiB)", + "relay_all": "Реле на всички получатели", + "relay_all_info": "↪ Ако изберете да не релеирате всички получатели, ще трябва да добавите (\"скрита\") пощенска кутия за всеки отделен получател, който трябва да бъде релеиран.", + "relay_domain": "Реле на този домейн", + "relay_transport_info": "
    Инфо
    Можете да дефинирате транспортни карти за персонализирана дестинация за този домейн. Ако не е зададено, ще бъде направен MX lookup.", + "relay_unknown_only": "Реле на несъществуващи пощенски кутии. Съществуващите пощенски кутии ще бъдат доставени локално.", + "relayhost_wrapped_tls_info": "Моля, не използвайте TLS-wrapped портове (най-често използвани на порт 465).
    \r\nИзползвайте всеки не-wrapped порт и издайте STARTTLS. Политика за TLS може да бъде създадена в \"TLS политики\", за да се наложи TLS.", + "select": "Моля, изберете...", + "select_domain": "Моля, изберете първо домейн", + "sieve_desc": "Кратко описание", + "sieve_type": "Тип на филтър", + "skipcrossduplicates": "Пропускане на дублирани съобщения между папки (първи дошъл, първи обслужен)", + "subscribeall": "Абониране за всички папки", + "syncjob": "Добавяне на синхронизираща задача", + "syncjob_hint": "Имайте предвид, че паролите трябва да бъдат запазени като обикновен текст!", + "tags": "Тагове", + "target_address": "Адреси за пренасочване", + "target_address_info": "Пълни имейл адреси (разделени с запетая).", + "target_domain": "Целеви домейн", + "timeout1": "Таймаут за връзка с отдалечен хост", + "timeout2": "Таймаут за връзка с локален хост", + "username": "Потребителско име", + "validate": "Валидиране", + "validation_success": "Успешно валидиране" + }, + "admin": { + "access": "Достъп", + "action": "Действие", + "activate_api": "Активиране на API", + "activate_send": "Активиране на бутона за изпращане", + "active": "Активен", + "active_rspamd_settings_map": "Активна карта с настройки", + "add": "Добавяне", + "add_admin": "Добавяне на администратор", + "add_domain_admin": "Добавяне на администратор на домейн", + "add_forwarding_host": "Добавяне на хост за препращане", + "add_relayhost": "Добавяне на транспорт, зависим от изпращач", + "add_relayhost_hint": "Имайте предвид, че данните за удостоверяване, ако има такива, ще бъдат запазени като обикновен текст.", + "add_row": "Добавяне на ред", + "add_settings_rule": "Добавяне на правило за настройки", + "add_transport": "Добавяне на транспорт", + "add_transports_hint": "Имайте предвид, че данните за удостоверяване, ако има такива, ще бъдат запазени като обикновен текст.", + "additional_rows": "добавени допълнителни редове", + "admin": "Администратор", + "admin_details": "Редактиране на детайлите на администратора", + "admin_domains": "Назначения на домейни", + "admins": "Администратори", + "admins_ldap": "LDAP администратори", + "advanced_settings": "Разширени настройки", + "allowed_methods": "Access-Control-Allow-Methods", + "allowed_origins": "Access-Control-Allow-Origin", + "api_allow_from": "Разрешаване на API достъп от тези IP адреси/CIDR мрежови нотации", + "api_info": "API е в процес на разработка. Документацията може да бъде намерена на /api", + "api_key": "API ключ", + "api_read_only": "Достъп само за четене", + "api_read_write": "Достъп за четене и писане", + "api_skip_ip_check": "Пропускане на IP проверка за API", + "app_hide": "Скриване за вход", + "app_links": "Връзки към приложения", + "app_name": "Име на приложението", + "apps_name": "Име на \"mailcow приложенията\"", + "arrival_time": "Време на пристигане (време на сървъра)", + "authed_user": "Удостоверен потребител", + "ays": "Сигурни ли сте, че искате да продължите?", + "ban_list_info": "Вижте списък с блокирани IP адреси по-долу: мрежа (оставащо време за блокиране) - [действия].
    IP адресите в опашката за разблокиране ще бъдат премахнати от активния списък с блокирани в рамките на няколко секунди.
    Червените етикети показват активни постоянни блокирания от черния списък.", + "change_logo": "Промяна на логото", + "logo_normal_label": "Нормално", + "logo_dark_label": "Обърнато за тъмен режим", + "configuration": "Конфигурация", + "convert_html_to_text": "Конвертиране на HTML в обикновен текст", + "copy_to_clipboard": "Текстът е копиран в клипборда!", + "cors_settings": "Настройки на CORS", + "credentials_transport_warning": "Внимание: Добавянето на нов запис в картата на транспорта ще обнови данните за удостоверяване за всички записи с съвпадаща колона за следващ хоп.", + "customer_id": "Идентификатор на клиента", + "customize": "Персонализиране", + "destination": "Дестинация", + "dkim_add_key": "Добавяне на ARC/DKIM ключ", + "dkim_domains_selector": "Селектор", + "dkim_domains_wo_keys": "Избор на домейни без ключове", + "dkim_from": "От", + "dkim_from_title": "Източник на домейн за копиране на данни", + "dkim_key_length": "Дължина на DKIM ключа (битове)", + "dkim_key_missing": "Липсващ ключ", + "dkim_key_unused": "Неизползван ключ", + "dkim_key_valid": "Валиден ключ", + "dkim_keys": "ARC/DKIM ключове", + "dkim_overwrite_key": "Презаписване на съществуващ DKIM ключ", + "dkim_private_key": "Частен ключ", + "dkim_to": "До", + "dkim_to_title": "Целеви домейн/и - ще бъде презаписан", + "domain": "Домейн", + "domain_admin": "Администратор на домейн", + "domain_admins": "Администратори на домейн", + "domain_s": "Домейн/и", + "duplicate": "Дублиране", + "duplicate_dkim": "Дублиране на DKIM запис", + "edit": "Редактиране", + "empty": "Няма резултати", + "excludes": "Изключване на тези получатели", + "f2b_ban_time": "Време за блокиране (с)", + "f2b_ban_time_increment": "Времето за блокиране се увеличава с всяко блокиране", + "f2b_blacklist": "Черни списъци/хостове", + "f2b_filter": "Филтри с регулярни изрази", + "f2b_list_info": "Черният списък на хостове или мрежи винаги ще надделява над белия списък. Актуализациите на списъка отнемат няколко секунди, за да бъдат приложени.", + "f2b_manage_external": "Управление на Fail2Ban външно", + "f2b_manage_external_info": "Fail2ban все още ще поддържа черния списък, но няма да задава правила за блокиране на трафика. Използвайте генерирания черн списък по-долу, за да блокирате трафика външно.", + "f2b_max_attempts": "Макс. опити", + "f2b_max_ban_time": "Макс. време за блокиране (с)", + "f2b_netban_ipv4": "IPv4 размер на подмрежа за прилагане на блокиране (8-32)", + "f2b_netban_ipv6": "IPv6 размер на подмрежа за прилагане на блокиране (8-128)", + "f2b_parameters": "Параметри на Fail2ban", + "f2b_regex_info": "Логовете, които се вземат предвид: SOGo, Postfix, Dovecot, PHP-FPM.", + "f2b_retry_window": "Прозорец за повторен опит (с) за макс. опити", + "f2b_whitelist": "Бели списъци/хостове", + "filter": "Филтър", + "filter_table": "Таблица с филтри", + "forwarding_hosts": "Хостове за препращане", + "forwarding_hosts_add_hint": "Можете да посочите IPv4/IPv6 адреси, мрежи в CIDR нотация, имена на хостове (които ще бъдат разрешени в IP адреси), или имена на домейни (които ще бъдат разрешени в IP адреси чрез заявки към SPF записи или, при липсата им, MX записи).", + "forwarding_hosts_hint": "Входящите съобщения се приемат безрезервно от всички хостове, изброени тук. Тези хостове след това не се проверяват срещу DNSBL или подлагат на сиво списък. Спамът, получен от тях, никога не се отхвърля, но опционално може да бъде поставен във файла за нежелана поща. Най-често използваната употреба за това е да се посочат пощенски сървъри, на които сте настроили правило, което препраща входящите имейли към вашия mailcow сървър.", + "from": "От", + "generate": "генериране", + "guid": "GUID - уникален инстанционен идентификатор", + "guid_and_license": "GUID & Лиценз", + "hash_remove_info": "Премахването на хеш за ограничение на скоростта (ако все още съществува) ще нулира неговия брояч напълно.
    \r\nВсеки хеш е показан с индивидуален цвят.", + "help_text": "Презаписване на помощния текст под маската за вход (разрешен е HTML)", + "host": "Хост", + "html": "HTML", + "iam": "Доставчик на идентичност", + "iam_attribute_field": "Поле за атрибут", + "iam_authorize_url": "Крайна точка за удостоверяване", + "iam_auth_flow": "Поток за удостоверяване", + "iam_auth_flow_info": "Освен стандартния поток за удостоверяване (поток с код за удостоверяване в Keycloak), който се използва за единичен вход, mailcow поддържа и поток за удостоверяване с пряки удостоверения. Потокът за удостоверяване с пароли за поща проверява удостоверенията на потребителя, като използва Keycloak Admin REST API. mailcow извлича хешираната парола от атрибута mailcow_password, който е картиран в Keycloak.", + "iam_basedn": "Основен DN", + "iam_client_id": "Идентификатор на клиента", + "iam_client_secret": "Тайна на клиента", + "iam_client_scopes": "Обхват на клиента", + "iam_default_template": "Шаблон по подразбиране", + "iam_default_template_description": "Ако няма зададен шаблон за потребител, шаблонът по подразбиране ще бъде използван за създаване на пощенската кутия, но не и за актуализиране на пощенската кутия.", + "iam_description": "Конфигуриране на външен доставчик за удостоверяване
    Потребителските пощенски кутии ще бъдат създавани автоматично при първия им вход, при условие че е зададено картиране на атрибути.", + "iam_extra_permission": "За да работят следните настройки, клиентът на mailcow в Keycloak трябва да има услуга акаунт и разрешението да вижда потребители.", + "iam_host": "Хост", + "iam_host_info": "Въведете един или повече LDAP хостове, разделени с запетаи.", + "iam_import_users": "Импортиране на потребители", + "iam_mapping": "Картиране на атрибути", + "iam_bindpass": "Парола за свързване", + "iam_periodic_full_sync": "Периодичен пълен синхрон", + "iam_port": "Порт", + "iam_realm": "Реалм", + "iam_redirect_url": "URL за пренасочване", + "iam_rest_flow": "Поток за пароли за поща", + "iam_server_url": "URL на сървъра", + "iam_sso": "Единичен вход", + "iam_sync_interval": "Интервал за синхронизиране/импортиране (мин)", + "iam_test_connection": "Тест на връзката", + "iam_token_url": "Крайна точка за токени", + "iam_userinfo_url": "Крайна точка за информация за потребителя", + "iam_username_field": "Поле за потребителско име", + "iam_binddn": "DN за свързване", + "iam_use_ssl": "Използване на SSL", + "iam_use_ssl_info": "Ако активирате SSL и портът е зададен на 389, той ще бъде автоматично променен на 636.", + "iam_use_tls": "Използване на StartTLS", + "iam_use_tls_info": "Ако активирате TLS, трябва да използвате стандартния порт за вашия LDAP сървър (389). SSL портовете не могат да бъдат използвани.", + "iam_version": "Версия", + "ignore_ssl_error": "Игнориране на SSL грешки", + "import": "Импортиране", + "import_private_key": "Импортиране на частен ключ", + "in_use_by": "Използван от", + "inactive": "Неактивен", + "include_exclude": "Включване/Изключване", + "include_exclude_info": "По подразбиране - без избор - всички пощенски кутии се адресират", + "includes": "Включване на тези получатели", + "ip_check": "Проверка на IP", + "ip_check_disabled": "Проверката на IP е деактивирана. Можете да я активирате в
    Система > Конфигурация > Опции > Персонализиране", + "ip_check_opt_in": "Оптиране за използване на услугата на трета страна ipv4.mailcow.email и ipv6.mailcow.email, за да разрешавате външни IP адреси.", + "is_mx_based": "MX базиран", + "last_applied": "Последно приложено", + "license_info": "Лицензът не е задължителен, но помага за по-нататъшното развитие.
    Регистрирайте вашия GUID тук или купете поддръжка за вашата инсталация на mailcow.", + "link": "Връзка", + "loading": "Моля, изчакайте...", + "login_time": "Време за вход", + "logo_info": "Вашето изображение ще бъде мащабирано до височина 40 px за горната навигационна лента и макс. ширина 250 px за началната страница. Препоръчва се използването на скалируема графика.", + "lookup_mx": "Дестинацията е регулярен израз за съвпадение с MX име (.*\\.google\\.com за пренасочване на всички съобщения, предназначени за MX, завършващ на google.com, през този хоп)", + "main_name": "Име на \"mailcow UI\"", + "merged_vars_hint": "Редовете в сиво са сляти от vars.(local.)inc.php и не могат да бъдат променени.", + "message": "Съобщение", + "message_size": "Размер на съобщението", + "nexthop": "Следващ хоп", + "no": "✕", + "no_active_bans": "Няма активни блокирания", + "no_new_rows": "Няма допълнителни редове", + "no_record": "Няма запис", + "oauth2_apps": "OAuth2 приложения", + "oauth2_add_client": "Добавяне на OAuth2 клиент", + "oauth2_client_id": "Идентификатор на клиента", + "oauth2_client_secret": "Тайна на клиента", + "oauth2_info": "Реализацията на OAuth2 поддържа типа разрешение \"Код за удостоверяване\" и издава токени за презареждане.
    \r\nСървърът автоматично издава нови токени за презареждане, след като токен за презареждане е бил използван.

    \r\n• Обхватът по подразбиране е профил. Само потребителите на пощенски кутии могат да бъдат удостоверени срещу OAuth2. Ако параметърът за обхват е пропуснат, той се връща към профил.
    \r\n• Параметърът state трябва да бъде изпратен от клиента като част от заявката за удостоверяване.

    \r\nПътища за заявки към OAuth2 API:
    \r\n
      \r\n
    • Крайна точка за удостоверяване: /oauth/authorize
    • \r\n
    • Крайна точка за токени: /oauth/token
    • \r\n
    • Страница с ресурси: /oauth/profile
    • \r\n
    \r\nГенерирането на нова тайна на клиента няма да анулира съществуващите кодове за удостоверяване, но те няма да могат да подновят своя токен.

    \r\nАнулирането на токените на клиента ще доведе до незабавно прекратяване на всички активни сесии. Всички клиенти ще трябва да се преудостоверят.", + "oauth2_redirect_uri": "URI за пренасочване", + "oauth2_renew_secret": "Генериране на нова тайна на клиента", + "oauth2_revoke_tokens": "Анулиране на всички токени на клиента", + "optional": "опционално", + "options": "Опции", + "password": "Парола", + "password_length": "Дължина на паролата", + "password_policy": "Политика за пароли", + "password_policy_chars": "Трябва да съдържа поне една буквена буква", + "password_policy_length": "Минималната дължина на паролата е %d", + "password_policy_lowerupper": "Трябва да съдържа малки и главни букви", + "password_policy_numbers": "Трябва да съдържа поне една цифра", + "password_policy_special_chars": "Трябва да съдържа специални символи", + "password_repeat": "Потвърждаване на паролата (повторете)", + "password_reset_info": "Ако не е зададен имейл за възстановяване, тази функция не може да бъде използвана.", + "password_reset_settings": "Настройки за възстановяване на парола", + "password_reset_tmpl_html": "HTML шаблон", + "password_reset_tmpl_text": "Текстов шаблон", + "password_settings": "Настройки на паролата", + "priority": "Приоритет", + "private_key": "Частен ключ", + "quarantine": "Карантина", + "quarantine_bcc": "Изпращане на копие от всички уведомления (BCC) до този получател:
    Оставете празно, за да деактивирате. Неподписано, непроверено имейл. Трябва да бъде доставено само вътрешно.", + "quarantine_exclude_domains": "Изключване на домейни и домейни на псевдоними", + "quarantine_max_age": "Максимална възраст в дни
    Стойността трябва да бъде равна или по-голяма от 1 ден.", + "quarantine_max_score": "Изхвърляне на уведомление, ако резултатът за спам на имейла е по-висок от тази стойност:
    По подразбиране е 9999.0", + "quarantine_max_size": "Максимален размер в MiB (по-големи елементи се изхвърлят):
    0 не означава неограничен.", + "quarantine_notification_html": "Шаблон на имейл за уведомление:
    Оставете празно, за да възстановите шаблона по подразбиране.", + "quarantine_notification_sender": "Изпращач на имейл за уведомление", + "quarantine_notification_subject": "Тема на имейл за уведомление", + "quarantine_redirect": "Пренасочване на всички уведомления към този получател:
    Оставете празно, за да деактивирате. Неподписано, непроверено имейл. Трябва да бъде доставено само вътрешно.", + "quarantine_release_format": "Формат на освободените елементи", + "quarantine_release_format_att": "Като прикачен файл", + "quarantine_release_format_raw": "Непроменен оригинал", + "quarantine_retention_size": "Задържания за пощенска кутия:
    0 показва неактивен.", + "quota_notification_html": "Шаблон на имейл за уведомление:
    Оставете празно, за да възстановите шаблона по подразбиране.", + "quota_notification_sender": "Изпращач на имейл за уведомление", + "quota_notification_subject": "Тема на имейл за уведомление", + "quota_notifications": "Уведомления за квота", + "quota_notifications_info": "Уведомленията за квота се изпращат на потребителите веднъж при преминаване на 80% и веднъж при преминаване на 95% използване.", + "quota_notifications_vars": "{{percent}} е равно на текущата квота на потребителя
    {{username}} е името на пощенската кутия", + "queue_unban": "разблокиране", + "r_active": "Активни ограничения", + "r_inactive": "Неактивни ограничения", + "r_info": "Елементите в сиво в списъка с активни ограничения не са известни като валидни ограничения за mailcow и не могат да бъдат преместени. Неизвестните ограничения ще бъдат зададени в реда на появяване все пак.
    Можете да добавяте нови елементи в inc/vars.local.inc.php, за да можете да ги превключвате.", + "rate_name": "Име на ограничението", + "recipients": "Получатели", + "refresh": "Опресняване", + "regen_api_key": "Регенериране на API ключ", + "regex_maps": "Карти с регулярни изрази", + "relay_from": "Адрес \"From:\"", + "relay_rcpt": "Адрес \"To:\"", + "relay_run": "Изпълнение на тест", + "relayhosts": "Транспорти, зависими от изпращач", + "relayhosts_hint": "Дефинирайте транспорти, зависими от изпращач, за да можете да ги избирате в диалоговия прозорец за конфигурация на домейн.
    \r\n Услугата за транспорт винаги е \"smtp:\" и ще се опита да използва TLS, ако бъде предложена. Wrapped TLS (SMTPS) не се поддържа. Политиката за TLS на потребителя ще бъде взета предвид.
    \r\n Засегнатите домейни, включително домейни на псевдоними.", + "remove": "Премахване", + "remove_row": "Премахване на ред", + "reset_default": "Нулиране до подразбиране", + "reset_limit": "Премахване на хеш", + "reset_password_vars": "{{link}} Генерираната връзка за нулиране на парола
    {{username}} Името на пощенската кутия на потребителя, който е поискал нулиране на парола
    {{username2}} Името на пощенската кутия за възстановяване
    {{date}} Датата на заявката за нулиране на парола
    {{token_lifetime}} Времетраенето на токена в минути
    {{hostname}} Името на хоста на mailcow", + "restore_template": "Оставете празно, за да възстановите шаблона по подразбиране.", + "routing": "Маршрутизация", + "rsetting_add_rule": "Добавяне на правило", + "rsetting_content": "Съдържание на правилото", + "rsetting_desc": "Кратко описание", + "rsetting_no_selection": "Моля, изберете правило", + "rsetting_none": "Няма налични правила", + "rsettings_insert_preset": "Вмъкване на примерен предефиниран набор \"%s\"", + "rsettings_preset_1": "Деактивиране на всичко, освен DKIM и ограничение на скоростта за удостоверени потребители", + "rsettings_preset_2": "Пощенските администратори искат спам", + "rsettings_preset_3": "Разрешаване само на определени изпращачи за пощенска кутия (напр. използване като вътрешна пощенска кутия)", + "rsettings_preset_4": "Деактивиране на Rspamd за домейн", + "rspamd_com_settings": "Името на настройката ще бъде автоматично генерирано, моля, вижте примерните предефинирани набори по-долу. За повече детайли вижте документацията на Rspamd", + "rspamd_global_filters": "Глобални карти на филтри", + "rspamd_global_filters_agree": "Ще бъда внимателен!", + "rspamd_global_filters_info": "Глобалните карти на филтри съдържат различни видове глобални черни и бели списъци.", + "rspamd_global_filters_regex": "Имената им обясняват тяхната цел. Всичкото съдържание трябва да съдържа валиден регулярен израз във формат \"/pattern/options\" (напр. /.+@domain\\.tld/i).
    \r\n Въпреки че се извършват основни проверки за всеки ред с регулярен израз, функционалността на Rspamd може да бъде нарушена, ако не успява да прочете синтаксиса коректно.
    \r\n Rspamd ще се опита да прочете съдържанието на картата при промяна. Ако срещнете проблеми, рестартирайте Rspamd, за да наложите презареждане на картата.
    Елементите в черния списък са изключени от карантина.", + "rspamd_settings_map": "Карта с настройки на Rspamd", + "sal_level": "Ниво на Moo", + "save": "Запазване на промените", + "search_domain_da": "Търсене на домейни", + "send": "Изпращане", + "sender": "Изпращач", + "service": "Услуга", + "service_id": "Идентификатор на услугата", + "source": "Източник", + "spamfilter": "Филтър за спам", + "subject": "Тема", + "success": "Успех", + "sys_mails": "Системни имейли", + "task": "Задача", + "text": "Текст", + "time": "Време", + "title": "Заглавие", + "title_name": "Име на уеб страницата на \"mailcow UI\"", + "to_top": "Обратно нагоре", + "transport_dest_format": "Регулярен израз или синтаксис: example.org, .example.org, *, box@example.org (няколко стойности могат да бъдат разделени с запетая)", + "transport_maps": "Карти за транспорт", + "transport_test_rcpt_info": "• Използвайте null@hosted.mailcow.de, за да тествате препращането към чуждестранна дестинация.", + "transports_hint": "• Записът в картата на транспорта пренарежда запис в картата на транспорт, зависим от изпращач.
    \r\n• Транспортите, базирани на MX, се използват предимно.
    \r\n• Настройките за политика на TLS на потребителя се игнорират и могат да бъдат приложени само чрез записи в картите на политиката на TLS.
    \r\n• Услугата за транспорт за дефинираните транспорти винаги е \"smtp:\" и ще се опита да използва TLS, ако бъде предложена. Wrapped TLS (SMTPS) не се поддържа.
    \r\n• Адресите, съвпадащи с \"/localhost$/\", винаги ще бъдат транспортирани през \"local:\", следователно записът \"*\" няма да се приложи към тези адреси.
    \r\n• За да определите удостоверения за следващ хоп \"[host]:25\", Postfix винаги заявява за \"host\", преди да търси за \"[host]:25\". Това поведение прави невъзможно да се използват \"host\" и \"[host]:25\" едновременно.", + "ui_footer": "Подвал (разрешен е HTML)", + "ui_header_announcement": "Обяви", + "ui_header_announcement_active": "Задаване на активна обява", + "ui_header_announcement_content": "Текст (разрешен е HTML)", + "ui_header_announcement_help": "Обявата е видима за всички влезли потребители и на екрана за вход на UI.", + "ui_header_announcement_select": "Избор на тип обява", + "ui_header_announcement_type": "Тип", + "ui_header_announcement_type_danger": "Много важно", + "ui_header_announcement_type_info": "Информация", + "ui_header_announcement_type_warning": "Важно", + "ui_texts": "Етикети и текстове на UI", + "unban_pending": "разблокиране в опашката", + "unchanged_if_empty": "Ако не е променено, оставете празно", + "upload": "Качване", + "username": "Потребителско име", + "user_link": "Връзка към потребител", + "validate_license_now": "Валидиране на GUID срещу лицензен сървър", + "verify": "Проверка", + "yes": "✓" + }, + "danger": { + "access_denied": "Достъпът е отказан или данните от формуляра са невалидни", + "alias_domain_invalid": "Домейнът на псевдонима %s е невалиден", + "alias_empty": "Адресът на псевдонима не трябва да е празен", + "alias_goto_identical": "Адресът на псевдонима и адресът за пренасочване не трябва да са идентични", + "alias_invalid": "Адресът на псевдонима %s е невалиден", + "aliasd_targetd_identical": "Домейнът на псевдонима не трябва да е равен на целевия домейн: %s", + "aliases_in_use": "Макс. псевдоними трябва да бъдат по-големи или равни на %d", + "app_name_empty": "Името на приложението не може да бъде празно", + "app_passwd_id_invalid": "Идентификаторът на паролата за приложението %s е невалиден", + "authsource_in_use": "Доставчикът на идентичност не може да бъде променен или изтрит, тъй като се използва в момента от един или повече потребители.", + "bcc_empty": "BCC дестинацията не може да бъде празна", + "bcc_exists": "Картата BCC %s съществува за тип %s", + "bcc_must_be_email": "BCC дестинацията %s не е валиден имейл адрес", + "comment_too_long": "Коментарът е твърде дълъг, разрешени са макс. 160 символа", + "cors_invalid_method": "Зададен е невалиден метод за разрешаване", + "cors_invalid_origin": "Зададен е невалиден източник за разрешаване", + "defquota_empty": "Квотата по подразбиране за пощенска кутия не трябва да бъде 0.", + "demo_mode_enabled": "Режимът на демонстрация е активиран", + "description_invalid": "Описанието на ресурса за %s е невалидно", + "dkim_domain_or_sel_exists": "Съществува DKIM ключ за \"%s\", който няма да бъде презаписан", + "dkim_domain_or_sel_invalid": "DKIM домейн или селектор е невалиден: %s", + "domain_cannot_match_hostname": "Домейнът не може да съвпада с името на хоста", + "domain_exists": "Домейнът %s вече съществува", + "domain_invalid": "Името на домейна е празно или невалидно", + "domain_not_empty": "Не може да бъде премахнат непразен домейн %s", + "domain_not_found": "Домейнът %s не е намерен", + "domain_quota_m_in_use": "Квотата на домейна трябва да бъде по-голяма или равна на %s MiB", + "extended_sender_acl_denied": "липсва ACL за задаване на външни адреси на изпращач", + "extra_acl_invalid": "Външният адрес на изпращач \"%s\" е невалиден", + "extra_acl_invalid_domain": "Външният изпращач \"%s\" използва невалиден домейн", + "fido2_verification_failed": "Проверката на FIDO2 е неуспешна: %s", + "file_open_error": "Файлът не може да бъде отворен за писане", + "filter_type": "Грешен тип на филтър", + "from_invalid": "Изпращачът не трябва да бъде празен", + "generic_server_error": "Възникна неочаквана грешка на сървъра. Моля, свържете се с вашия администратор.", + "global_filter_write_error": "Файлът с филтър не може да бъде записан: %s", + "global_map_invalid": "Идентификаторът на глобалната карта %s е невалиден", + "global_map_write_error": "Глобалният идентификатор на картата %s не може да бъде записан: %s", + "goto_empty": "Адресът на псевдоним трябва да съдържа поне един валиден адрес за пренасочване", + "goto_invalid": "Адресът за пренасочване %s е невалиден", + "ham_learn_error": "Грешка при обучение за не-спам: %s", + "iam_test_connection": "Връзката е неуспешна", + "imagick_exception": "Грешка: Грешка при четене на изображение с Imagick", + "img_dimensions_exceeded": "Изображението надхвърля максималния размер", + "img_invalid": "Не може да се валидира файл с изображение", + "img_size_exceeded": "Изображението надхвърля максималния размер на файла", + "img_tmp_missing": "Не може да се валидира файл с изображение: Липсва временен файл", + "invalid_bcc_map_type": "Невалиден тип на картата BCC", + "invalid_destination": "Форматът на дестинацията \"%s\" е невалиден", + "invalid_filter_type": "Невалиден тип на филтър", + "invalid_host": "Зададен е невалиден хост: %s", + "invalid_mime_type": "Невалиден MIME тип", + "invalid_nexthop": "Форматът на следващия хоп е невалиден", + "invalid_nexthop_authenticated": "Следващият хоп съществува с различни удостоверения, моля, актуализирайте съществуващите удостоверения за този следващ хоп първо.", + "invalid_recipient_map_new": "Зададен е невалиден нов получател: %s", + "invalid_recipient_map_old": "Зададен е невалиден оригинален получател: %s", + "invalid_reset_token": "Невалиден токен за нулиране", + "ip_list_empty": "Списъкът с разрешени IP адреси не може да бъде празен", + "is_alias": "%s вече е известен като адрес на псевдоним", + "is_alias_or_mailbox": "%s вече е известен като адрес на псевдоним, пощенска кутия или адрес на псевдоним, разширен от домейн на псевдоним.", + "is_spam_alias": "%s вече е известен като временен адрес на псевдоним (спам адрес на псевдоним)", + "last_key": "Последното ключове не може да бъде изтрито, моля, деактивирайте TFA вместо това.", + "login_failed": "Неуспешен вход", + "mailbox_defquota_exceeds_mailbox_maxquota": "Квотата по подразбиране надхвърля лимита на макс. квота", + "mailbox_invalid": "Името на пощенската кутия е невалидно", + "mailbox_quota_exceeded": "Квотата надхвърля лимита на домейна (макс. %d MiB)", + "mailbox_quota_exceeds_domain_quota": "Макс. квотата надхвърля лимита на квотата на домейна", + "mailbox_quota_left_exceeded": "Няма достатъчно място (оставащо място: %d MiB)", + "mailboxes_in_use": "Макс. пощенски кутии трябва да бъдат по-големи или равни на %d", + "malformed_username": "Неправилно формирано потребителско име", + "map_content_empty": "Съдържанието на картата не може да бъде празно", + "max_alias_exceeded": "Макс. псевдоними са надхвърлени", + "max_mailbox_exceeded": "Макс. пощенски кутии са надхвърлени (%d от %d)", + "max_quota_in_use": "Квотата на пощенската кутия трябва да бъде по-голяма или равна на %d MiB", + "maxquota_empty": "Макс. квотата за пощенска кутия не трябва да бъде 0.", + "mysql_error": "Грешка на MySQL: %s", + "network_host_invalid": "Невалидна мрежа или хост: %s", + "next_hop_interferes": "%s се намесва с nexthop %s", + "next_hop_interferes_any": "Съществуващ следващ хоп се намесва с %s", + "nginx_reload_failed": "Презареждането на Nginx е неуспешно: %s", + "no_user_defined": "Няма зададен потребител", + "object_exists": "Обектът %s вече съществува", + "object_is_not_numeric": "Стойността %s не е числова", + "password_complexity": "Паролата не отговаря на изискванията", + "password_empty": "Паролата не трябва да бъде празна", + "password_mismatch": "Паролата за потвърждение не съвпада", + "password_reset_invalid_user": "Пощенската кутия не е намерена или не е зададен имейл за възстановяване", + "password_reset_na": "Възстановяването на парола в момента е недостъпно. Моля, свържете се с вашия администратор.", + "policy_list_from_exists": "Съществува запис с даденото име", + "policy_list_from_invalid": "Записът има невалиден формат", + "private_key_error": "Грешка с частен ключ: %s", + "pushover_credentials_missing": "Липсва токен или ключ на Pushover", + "pushover_key": "Ключът на Pushover има грешен формат", + "pushover_token": "Токенът на Pushover има грешен формат", + "quota_not_0_not_numeric": "Квотата трябва да бъде числова и >= 0", + "recipient_map_entry_exists": "Съществува запис в картата на получатели \"%s\"", + "recovery_email_failed": "Не може да бъде изпратен имейл за възстановяване. Моля, свържете се с вашия администратор.", + "redis_error": "Грешка на Redis: %s", + "relayhost_invalid": "Записът в картата %s е невалиден", + "release_send_failed": "Съобщението не може да бъде освободено: %s", + "required_data_missing": "Липсват необходими данни %s", + "reset_f2b_regex": "Регулярният филтър не може да бъде нулиран навреме, моля, опитайте отново или изчакайте още няколко секунди и презаредете уеб сайта.", + "reset_token_limit_exceeded": "Лимитът на токена за нулиране е надхвърлен. Моля, опитайте по-късно.", + "resource_invalid": "Името на ресурса %s е невалидно", + "rl_timeframe": "Временният интервал за ограничение на скоростта е некоректен", + "rspamd_ui_pw_length": "Паролата на UI на Rspamd трябва да бъде поне 6 символа", + "script_empty": "Скриптът не може да бъде празен", + "sender_acl_invalid": "Стойността на ACL на изпращач %s е невалидна", + "set_acl_failed": "Неуспешно задаване на ACL", + "settings_map_invalid": "Идентификаторът на картата с настройки %s е невалиден", + "sieve_error": "Грешка при синтаксиса на Sieve: %s", + "spam_learn_error": "Грешка при обучение за спам: %s", + "subject_empty": "Темата не трябва да бъде празна", + "target_domain_invalid": "Целевият домейн %s е невалиден", + "targetd_not_found": "Целевият домейн %s не е намерен", + "targetd_relay_domain": "Целевият домейн %s е домейн за реле", + "template_exists": "Шаблонът %s вече съществува", + "template_id_invalid": "Идентификаторът на шаблона %s е невалиден", + "template_name_invalid": "Името на шаблона е невалидно", + "temp_error": "Временна грешка", + "text_empty": "Текстът не трябва да бъде празен", + "tfa_token_invalid": "Токенът за TFA е невалиден", + "tls_policy_map_dest_invalid": "Дестинацията на политиката е невалидна", + "tls_policy_map_entry_exists": "Съществува запис в картата на политиката на TLS \"%s\"", + "tls_policy_map_parameter_invalid": "Параметърът на политиката е невалиден", + "to_invalid": "Получателят не трябва да бъде празен", + "totp_verification_failed": "Проверката на TOTP е неуспешна", + "transport_dest_exists": "Дестинацията на транспорта \"%s\" съществува", + "webauthn_verification_failed": "Проверката на WebAuthn е неуспешна: %s", + "webauthn_authenticator_failed": "Избраният аутентикатор не е намерен", + "webauthn_publickey_failed": "Не е запазен публичен ключ за избрания аутентикатор", + "webauthn_username_failed": "Избраният аутентикатор принадлежи на друга сметка", + "unknown": "Възникна неизвестна грешка", + "unknown_tfa_method": "Неизвестен метод за TFA", + "unlimited_quota_acl": "Неограничената квота е забранена от ACL", + "username_invalid": "Потребителското име %s не може да бъде използвано", + "validity_missing": "Моля, задайте период на валидност", + "value_missing": "Моля, предоставете всички стойности", + "yotp_verification_failed": "Проверката на Yubico OTP е неуспешна: %s" + }, + "datatables": { + "collapse_all": "Свиване на всичко", + "decimal": ".", + "emptyTable": "Няма данни в таблицата", + "expand_all": "Разширяване на всичко", + "info": "Показване на _START_ до _END_ от _TOTAL_ записа", + "infoEmpty": "Показване на 0 до 0 от 0 записа", + "infoFiltered": "(филтрирани от общо _MAX_ записа)", + "infoPostFix": "", + "thousands": ",", + "lengthMenu": "Показване на _MENU_ записа", + "loadingRecords": "Зареждане...", + "processing": "Моля, изчакайте...", + "search": "Търсене:", + "zeroRecords": "Няма съвпадащи записи", + "paginate": { + "first": "Първи", + "last": "Последен", + "next": "Следващ", + "previous": "Предишен" + }, + "aria": { + "sortAscending": ": активиране за сортиране на колоната във възходящ ред", + "sortDescending": ": активиране за сортиране на колоната в низходящ ред" + } + }, + "debug": { + "architecture": "Архитектура", + "chart_this_server": "Графика (този сървър)", + "containers_info": "Информация за контейнерите", + "container_running": "Изпълнява се", + "container_disabled": "Контейнерът е спрян или деактивиран", + "container_stopped": "Спрян", + "cores": "Ядра", + "current_time": "Системно време", + "disk_usage": "Използване на диска", + "docs": "Документи", + "error_show_ip": "Не може да се разреши публичния IP адрес", + "external_logs": "Външни логове", + "history_all_servers": "История (всички сървъри)", + "in_memory_logs": "Логове в паметта", + "last_modified": "Последно модифициране", + "log_info": "

    Логовете на mailcow в паметта се събират в списъци на Redis и се подрязват до LOG_LINES (%d) на всяка минута, за да се намали натоварването.
    Логовете в паметта не са предназначени да бъдат постоянни. Всички приложения, които записват в паметта, също записват в демона на Docker и следователно в основния драйвер за логсване.
    Логовете в паметта трябва да се използват за отстраняване на леки проблеми с контейнери.

    \r\n

    Външните логове се събират чрез API на даденото приложение.

    \r\n

    Статичните логове са най-вече логове на активност, които не се записват в демона на Docker, но все още трябва да бъдат постоянни (с изключение на логовете на API).

    ", + "login_time": "Време", + "logs": "Логове", + "memory": "Памет", + "online_users": "Потребители онлайн", + "restart_container": "Рестартиране", + "service": "Услуга", + "show_ip": "Показване на публичен IP", + "size": "Размер", + "started_at": "Стартирано в", + "started_on": "Стартирано на", + "static_logs": "Статични логове", + "success": "Успех", + "system_containers": "Система & Контейнери", + "timezone": "Часова зона", + "uptime": "Време на работа", + "update_available": "Има налична актуализация", + "no_update_available": "Системата е на последното издание", + "update_failed": "Не може да се провери за актуализация", + "username": "Потребителско име", + "wip": "В момента се работи" + }, + "diagnostics": { + "cname_from_a": "Стойността е извлечена от A/AAAA запис. Това се поддържа, докато записът сочи към правилния ресурс.", + "dns_records": "DNS записи", + "dns_records_24hours": "Моля, имайте предвид, че промените в DNS може да отнемат до 24 часа, за да имат актуалното си състояние правилно отразено на тази страница. Тя е предназначена да ви позволи лесно да видите как да конфигурирате вашите DNS записи и да проверите дали всичките ви записи са правилно запазени в DNS.", + "dns_records_data": "Правилни данни", + "dns_records_docs": "Моля, вижте и документацията.", + "dns_records_name": "Име", + "dns_records_status": "Текущо състояние", + "dns_records_type": "Тип", + "optional": "Този запис е опционален." + }, + "edit": { + "acl": "ACL (Разрешение)", + "active": "Активен", + "admin": "Редактиране на администратор", + "advanced_settings": "Разширени настройки", + "alias": "Редактиране на псевдоним", + "allow_from_smtp": "Разрешаване само на тези IP адреси да използват SMTP", + "allow_from_smtp_info": "Оставете празно, за да разрешите всички изпращачи.
    IPv4/IPv6 адреси и мрежи.", + "allowed_protocols": "Разрешени протоколи за пряк потребителски достъп (не засяга протоколите за пароли на приложения)", + "app_name": "Име на приложението", + "app_passwd": "Парола за приложение", + "app_passwd_protocols": "Разрешени протоколи за паролата на приложението", + "automap": "Опит за автоматично картографиране на папки (\"Изпратени\", \"Изпратени\" => \"Изпратени\" и т.н.)", + "backup_mx_options": "Опции за реле", + "bcc_dest_format": "BCC дестинацията трябва да бъде един валиден имейл адрес.
    Ако имате нужда да изпратите копие до множество адреси, създайте псевдоним и го използвайте тук.", + "client_id": "Идентификатор на клиента", + "client_secret": "Тайна на клиента", + "comment_info": "Частен коментар не е видим за потребителя, докато публичен коментар се показва като подсказка при преминаване с мишката върху него в прегледа на потребителя", + "created_on": "Създаден на", + "custom_attributes": "Персонализирани атрибути", + "delete1": "Изтриване от източника след завършване", + "delete2": "Изтриване на съобщенията в дестинацията, които не са в източника", + "delete2duplicates": "Изтриване на дубликати в дестинацията", + "delete_ays": "Моля, потвърдете процеса на изтриване.", + "description": "Описание", + "disable_login": "Забраняване на вход (входящите съобщения все още се приемат)", + "domain": "Редактиране на домейн", + "domain_admin": "Редактиране на администратор на домейн", + "domain_footer": "Подвал за домейн", + "domain_footer_html": "HTML подвал", + "domain_footer_info": "Подвалите за домейн се добавят към всички изходящи имейли, свързани с адрес в този домейн. Следните променливи могат да бъдат използвани за подвала:", + "domain_footer_info_vars": { + "auth_user": "{= auth_user =} - Удостоверен потребител, посочен от MTA", + "from_user": "{= from_user =} - Потребителска част на обвивката, напр. за \"moo@mailcow.tld\" връща \"moo\"", + "from_name": "{= from_name =} - Име на обвивката, напр. за \"Mailcow <moo@mailcow.tld>\" връща \"Mailcow\"", + "from_addr": "{= from_addr =} - Адресна част на обвивката", + "from_domain": "{= from_domain =} - Домейна част на обвивката", + "custom": "{= foo =} - Ако пощенската кутия има персонализиран атрибут \"foo\" със стойност \"bar\", ще върне \"bar\"" + }, + "domain_footer_plain": "PLAIN подвал", + "domain_footer_skip_replies": "Игнориране на подвал в отговорни имейли", + "domain_quota": "Квота на домейна", + "domains": "Домейни", + "dont_check_sender_acl": "Деактивиране на проверката на изпращач за домейн %s (+ домейни на псевдоними)", + "edit_alias_domain": "Редактиране на домейн на псевдоним", + "encryption": "Криптиране", + "exclude": "Изключване на обекти (regex)", + "extended_sender_acl": "Външни адреси на изпращач", + "extended_sender_acl_info": "Трябва да бъде импортиран DKIM домейн ключ, ако е наличен.
    \r\n Помнете да добавите този сървър към съответния SPF TXT запис.
    \r\n Когато домейн или домейн на псевдоним бъде добавен към този сървър, който се припокрива с външен адрес, външният адрес ще бъде премахнат.
    \r\n Използвайте @domain.tld, за да разрешите изпращане като *@domain.tld.", + "footer_exclude": "Изключване от подвал", + "force_pw_update": "Принудително обновяване на парола при следващия вход", + "force_pw_update_info": "Този потребител ще може да влезе само в %s. Паролите за приложения остават използваеми.", + "full_name": "Пълно име", + "gal": "Глобален адресен списък", + "gal_info": "Глобалният адресен списък съдържа всички обекти на домейна и не може да бъде редактиран от нито един потребител. Липсва информация за заетост/свободно време в SOGo, ако е деактивирано! Рестартирайте SOGo, за да приложите промените.", + "generate": "генериране", + "grant_types": "Типове разрешения", + "hostname": "Име на хост", + "inactive": "Неактивен", + "kind": "Вид", + "last_modified": "Последно модифициране", + "lookup_mx": "Дестинацията е регулярен израз за съвпадение с MX име (.*\\.google\\.com за пренасочване на всички съобщения, предназначени за MX, завършващ на google.com, през този хоп)", + "mailbox": "Редактиране на пощенска кутия", + "mailbox_quota_def": "Квота по подразбиране за пощенска кутия", + "mailbox_relayhost_info": "Прилага се за пощенската кутия и директните псевдоними само, пренарежда запис в картата на транспорта за домейн.", + "mailbox_rename": "Преименуване на пощенска кутия", + "mailbox_rename_agree": "Създадох резервно копие.", + "mailbox_rename_alias": "Автоматично създаване на псевдоним", + "mailbox_rename_title": "Ново локално име на пощенска кутия", + "mailbox_rename_warning": "ВНИМАНИЕ! Създайте резервно копие преди преименуване на пощенската кутия.", + "max_aliases": "Макс. псевдоними", + "max_mailboxes": "Макс. възможни пощенски кутии", + "max_quota": "Макс. квота за пощенска кутия (MiB)", + "maxage": "Максимална възраст на съобщенията в дни, които ще бъдат проверени от отдалечен
    (0 = игнорирай възраст)", + "maxbytespersecond": "Макс. байтове за секунда
    (0 = неограничен)", + "mbox_rl_info": "Това ограничение на скоростта се прилага върху SASL името за вход, то съвпада с всеки \"от\" адрес, използван от влезлия потребител. Ограничението на скоростта на пощенска кутия пренарежда ограничението на скоростта за домейн.", + "mins_interval": "Интервал (мин)", + "multiple_bookings": "Множествени резервации", + "nexthop": "Следващ хоп", + "none_inherit": "Без / Наследяване", + "password": "Парола", + "password_recovery_email": "Имейл за възстановяване на парола", + "password_repeat": "Потвърждаване на паролата (повторете)", + "previous": "Предишна страница", + "private_comment": "Частен коментар", + "public_comment": "Публичен коментар", + "pushover": "Pushover", + "pushover_evaluate_x_prio": "Ескалиране на високоприоритетни съобщения [X-Priority: 1]", + "pushover_info": "Настройките за известия на Pushover ще се прилагат за всички чисти (не-спам) съобщения, доставени до %s, включително псевдоними (споделени, несподелени, таговани).", + "pushover_only_x_prio": "Разглеждане само на високоприоритетни съобщения [X-Priority: 1]", + "pushover_sender_array": "Разглеждане само на следните адреси на изпращач (разделени с запетая)", + "pushover_sender_regex": "Разглеждане на следния regex на изпращач", + "pushover_sound": "Звук", + "pushover_text": "Текст на известието", + "pushover_title": "Заглавие на известието", + "pushover_vars": "Когато не е зададен филтър на изпращач, всички съобщения ще бъдат разглеждани.
    Филтрите с регулярен израз, както и точните проверки на изпращач, могат да бъдат дефинирани индивидуално и ще бъдат разглеждани последователно. Те не зависят един от друг.
    Използвайте следните променливи за текст и заглавие (моля, вземете предвид политиките за поверителност на данните)", + "pushover_verify": "Проверка на удостоверения", + "quota_mb": "Квота (MiB)", + "quota_warning_bcc": "Предупреждения за квота ще бъдат изпращани като отделни копия до следните получатели:", + "quota_warning_bcc_info": "Съобщенията ще бъдат изпратени с тема, допълнена с името на потребителя в скоби, например: Предупреждение за квота (user@example.com).", + "ratelimit": "Ограничение на скоростта", + "redirect_uri": "URI за пренасочване/обратно извикване", + "relay_all": "Реле на всички получатели", + "relay_all_info": "↪ Ако изберете да не релеирате всички получатели, ще трябва да добавите (\"скрита\") пощенска кутия за всеки отделен получател, който трябва да бъде релеиран.", + "relay_domain": "Реле на този домейн", + "relay_transport_info": "
    Инфо
    Можете да дефинирате транспортни карти за персонализирана дестинация за този домейн. Ако не е зададено, ще бъде направен MX lookup.", + "relay_unknown_only": "Реле на несъществуващи пощенски кутии. Съществуващите пощенски кутии ще бъдат доставени локално.", + "relayhost": "Транспорти, зависими от изпращач", + "remove": "Премахване", + "resource": "Ресурс", + "save": "Запазване на промените", + "scope": "Обхват", + "sender_acl": "Разрешаване на изпращане като", + "sender_acl_disabled": "Проверката на изпращач е деактивирана", + "sender_acl_info": "Ако потребителят на пощенска кутия A е разрешен да изпраща като потребителя на пощенска кутия B, адресът на изпращач не се показва автоматично като избираем \"от\" поле в SOGo.
    \r\n Потребителят на пощенска кутия B трябва да създаде делегация в SOGo, за да разреши на потребителя на пощенска кутия A да избира техния адрес като изпращач. За да делегирате пощенска кутия в SOGo, използвайте менюто (три точки) вдясно от името на пощенската кутия в горния ляв ъгъл, докато сте в режим на поща. Това поведение не се прилага за адреси на псевдоними.", + "sieve_desc": "Кратко описание", + "sieve_type": "Тип на филтър", + "skipcrossduplicates": "Пропускане на дублирани съобщения между папки (първи дошъл, първи обслужен)", + "sogo_access": "Директно препращане към SOGo", + "sogo_access_info": "След влизане, потребителят се пренасочва автоматично към SOGo.", + "sogo_visible": "Псевдонимът е видим в SOGo", + "sogo_visible_info": "Тази опция засяга само обекти, които могат да бъдат показани в SOGo (споделени или несподелени адреси на псевдоними, сочещи поне една локална пощенска кутия). Ако е скрит, псевдонимът няма да се появи като избираем адрес на изпращач в SOGo.", + "spam_alias": "Създаване или промяна на временни псевдоними", + "spam_filter": "Филтър за спам", + "spam_policy": "Добавяне или премахване на елементи към черния/бял списък", + "spam_score": "Задаване на персонализиран резултат за спам", + "subfolder2": "Синхронизиране в подпапка в дестинацията
    (празно = не използвай подпапка)", + "syncjob": "Редактиране на синхронизираща задача", + "target_address": "Адрес/и за пренасочване (разделени с запетая)", + "target_domain": "Целеви домейн", + "timeout1": "Таймаут за връзка с отдалечен хост", + "timeout2": "Таймаут за връзка с локален хост", + "title": "Редактиране на обект", + "unchanged_if_empty": "Ако не е променено, оставете празно", + "username": "Потребителско име", + "validate_save": "Валидиране и запазване" + }, + "fido2": { + "confirm": "Потвърждаване", + "fido2_auth": "Вход с FIDO2", + "fido2_success": "Устройството е регистрирано успешно", + "fido2_validation_failed": "Проверката е неуспешна", + "fn": "Приятелско име", + "known_ids": "Известни идентификатори", + "none": "Деактивирано", + "register_status": "Състояние на регистрация", + "rename": "Преименуване", + "set_fido2": "Регистриране на FIDO2 устройство", + "set_fido2_touchid": "Регистриране на Touch ID на Apple M1", + "set_fn": "Задаване на приятелско име", + "start_fido2_validation": "Стартиране на проверка на FIDO2" + }, + "footer": { + "cancel": "Отказ", + "confirm_delete": "Потвърждаване на изтриването", + "delete_now": "Изтриване сега", + "delete_these_items": "Моля, потвърдете промените за следния идентификатор на обект", + "hibp_check": "Проверка срещу haveibeenpwned.com", + "hibp_nok": "Съвпадение! Това е потенциално опасна парола!", + "hibp_ok": "Няма съвпадение.", + "loading": "Моля, изчакайте...", + "nothing_selected": "Няма избрани елементи", + "restart_container": "Рестартиране на контейнер", + "restart_container_info": "Важно: Грациозното рестартиране може да отнеме известно време за завършване, моля, изчакайте да приключи.", + "restart_now": "Рестартиране сега", + "restarting_container": "Рестартиране на контейнера, това може да отнеме известно време" + }, + "header": { + "administration": "Конфигурация & Детайли", + "apps": "Приложения", + "debug": "Информация", + "email": "Имейл", + "mailcow_system": "Система", + "mailcow_config": "Конфигурация", + "quarantine": "Карантина", + "restart_netfilter": "Рестартиране на netfilter", + "restart_sogo": "Рестартиране на SOGo", + "user_settings": "Настройки на потребителя" + }, + "info": { + "awaiting_tfa_confirmation": "Изчакване на потвърждение за TFA", + "no_action": "Няма приложимо действие", + "session_expires": "Вашата сесия ще изтече след около 15 секунди" + }, + "login": { + "back_to_mailcow": "Обратно към mailcow", + "delayed": "Входът е забавен с %s секунди.", + "fido2_webauthn": "Вход с FIDO2/WebAuthn", + "forgot_password": "> Забравена парола?", + "invalid_pass_reset_token": "Токенът за нулиране на парола е невалиден или е изтекъл.
    Моля, заявете нов линк за нулиране на парола.", + "login": "Вход", + "login_admin": "Вход на администратор", + "login_dadmin": "Вход на администратор на домейн", + "login_user": "Потребителски вход", + "mobileconfig_info": "Моля, влезте като потребител на пощенска кутия, за да изтеглите поискания Apple профил за връзка.", + "new_password": "Нова парола", + "new_password_confirm": "Потвърждаване на новата парола", + "other_logins": "или вход с", + "password": "Парола", + "request_reset_password": "Заявка за промяна на парола", + "reset_password": "Нулиране на парола", + "username": "Потребителско име" + }, + "mailbox": { + "action": "Действие", + "activate": "Активиране", + "active": "Активен", + "add": "Добавяне", + "add_alias": "Добавяне на псевдоним", + "add_alias_expand": "Разширяване на псевдоним над домейни на псевдоними", + "add_bcc_entry": "Добавяне на запис в картата BCC", + "add_domain": "Добавяне на домейн", + "add_domain_alias": "Добавяне на псевдоним на домейн", + "add_domain_record_first": "Моля, добавете първо домейн", + "add_filter": "Добавяне на филтър", + "add_mailbox": "Добавяне на пощенска кутия", + "add_recipient_map_entry": "Добавяне на запис в картата на получатели", + "add_resource": "Добавяне на ресурс", + "add_template": "Добавяне на шаблон", + "add_tls_policy_map": "Добавяне на карта на политиката на TLS", + "address_rewriting": "Презаписване на адрес", + "alias": "Псевдоним", + "alias_domain_alias_hint": "Псевдонимите не се прилагат автоматично към домейни на псевдоними. Псевдонимът my-alias@domain не покрива адреса my-alias@alias-domain (където \"alias-domain\" е имагинерен псевдоним на домейн).
    Моля, използвайте филтър на Sieve, за да пренасочите имейл към външна пощенска кутия (вижте раздела \"Филтри\" или SOGo -> Пренасочвач). Използвайте \"Разширяване на псевдоним над домейни на псевдоними\", за да добавите автоматично липсващи псевдоними.", + "alias_domain_backupmx": "Псевдонимът на домейн е неактивен за домейн за реле", + "aliases": "Псевдоними", + "all_domains": "Всички домейни", + "allow_from_smtp": "Разрешаване само на тези IP адреси да използват SMTP", + "allow_from_smtp_info": "Оставете празно, за да разрешите всички изпращачи.
    IPv4/IPv6 адреси и мрежи.", + "allowed_protocols": "Разрешени протоколи", + "backup_mx": "Домейн за реле", + "bcc": "BCC", + "bcc_destination": "BCC дестинация", + "bcc_destinations": "BCC дестинации", + "bcc_info": "Картите BCC се използват, за да препращат тихо копия от всички съобщения до друг адрес. Записът от тип получател на карта се използва, когато локалната дестинация действа като получател на имейл. Записът от тип изпращач на карта се използва, когато локалната дестинация действа като изпращач на имейл. Локалната дестинация няма да бъде информирана за неуспешна доставка.", + "bcc_local_dest": "Локална дестинация", + "bcc_map": "Карта BCC", + "bcc_map_type": "Тип BCC", + "bcc_maps": "Карти BCC", + "bcc_rcpt_map": "Карта на получател", + "bcc_sender_map": "Карта на изпращач", + "bcc_to_rcpt": "Превключване към карта на получател", + "bcc_to_sender": "Превключване към карта на изпращач", + "bcc_type": "Тип BCC", + "booking_null": "Винаги показване като свободен", + "booking_0_short": "Винаги свободен", + "booking_custom": "Твърдо ограничение до определен брой резервации", + "booking_custom_short": "Твърдо ограничение", + "booking_ltnull": "Неограничено, но показване като заето, когато е резервирано", + "booking_lt0_short": "Меко ограничение", + "catch_all": "Catch-All", + "created_on": "Създаден на", + "daily": "Ежедневно", + "deactivate": "Деактивиране", + "description": "Описание", + "disable_login": "Забраняване на вход (входящите съобщения все още се приемат)", + "disable_x": "Деактивиране", + "dkim_domains_selector": "Селектор", + "dkim_key_length": "Дължина на DKIM ключа (битове)", + "domain": "Домейн", + "domain_admins": "Администратори на домейн", + "domain_aliases": "Псевдоними на домейн", + "domain_templates": "Шаблони за домейн", + "domain_quota": "Квота", + "domain_quota_total": "Обща квота на домейна", + "domains": "Домейни", + "edit": "Редактиране", + "empty": "Няма резултати", + "enable_x": "Активиране", + "excludes": "Изключване", + "filter_table": "Таблица с филтри", + "filters": "Филтри", + "fname": "Пълно име", + "force_pw_update": "Принудително обновяване на парола при следващия вход", + "gal": "Глобален адресен списък", + "goto_ham": "Учен като не е спам", + "goto_spam": "Учен като спам", + "hourly": "Часово", + "iam": "Доставчик на идентичност", + "in_use": "В употреба (%)", + "inactive": "Неактивен", + "insert_preset": "Вмъкване на примерен предефиниран набор \"%s\"", + "kind": "Вид", + "last_mail_login": "Последен вход в пощата", + "last_modified": "Последно модифициране", + "last_pw_change": "Последна промяна на парола", + "last_run": "Последно изпълнение", + "last_run_reset": "Задаване на следващо", + "mailbox": "Пощенска кутия", + "mailbox_defaults": "Настройки по подразбиране", + "mailbox_defaults_info": "Дефинирайте настройки по подразбиране за нови пощенски кутии.", + "mailbox_defquota": "Размер по подразбиране на пощенска кутия", + "mailbox_templates": "Шаблони за пощенска кутия", + "mailbox_quota": "Макс. размер на пощенска кутия", + "mailboxes": "Пощенски кутии", + "max_aliases": "Макс. псевдоними", + "max_mailboxes": "Макс. възможни пощенски кутии", + "max_quota": "Макс. квота за пощенска кутия", + "mins_interval": "Интервал (мин)", + "msg_num": "Съобщение №", + "multiple_bookings": "Множествени резервации", + "never": "Никога", + "no": "✕", + "no_record": "Няма запис за обект %s", + "no_record_single": "Няма запис", + "open_logs": "Отваряне на логове", + "owner": "Собственик", + "private_comment": "Частен коментар", + "public_comment": "Публичен коментар", + "q_add_header": "при преместване в папката за нежелана поща", + "q_all": " при преместване в папката за нежелана поща и при отхвърляне", + "q_reject": "при отхвърляне", + "quarantine_category": "Категория на уведомленията за карантина", + "quarantine_notification": "Уведомления за карантина", + "quick_actions": "Действия", + "recipient": "Получател", + "recipient_map": "Карта на получател", + "recipient_map_info": "Картите на получател се използват, за да се замени адресът на дестинация на съобщението, преди да бъде доставено.", + "recipient_map_new": "Нов получател", + "recipient_map_new_info": "Дестинацията на картата на получател трябва да бъде валиден имейл адрес или име на домейн.", + "recipient_map_old": "Оригинален получател", + "recipient_map_old_info": "Оригиналната дестинация на картата на получател трябва да бъде валиден имейл адрес или име на домейн.", + "recipient_maps": "Карти на получател", + "relay_all": "Реле на всички получатели", + "relay_unknown": "Реле на несъществуващи пощенски кутии", + "remove": "Премахване", + "resources": "Ресурси", + "running": "Изпълнява се", + "sender": "Изпращач", + "set_postfilter": "Маркиране като постфилтър", + "set_prefilter": "Маркиране като префилтър", + "sieve_info": "Можете да запазите множество филтри за всеки потребител, но само един префилтър и един постфилтър могат да бъдат активни едновременно.
    \r\nВсеки филтър ще бъде обработен в описания ред. Нито един неуспешен скрипт, нито издадена команда \"keep;\" няма да спре обработката на следващите скриптове. Промените в глобалните филтри на sieve ще предизвикат рестартиране на Dovecot.

    Глобален префилтър на sieve • Префилтър • Потребителски скриптове • Постфилтър • Глобален постфилтър на sieve", + "sieve_preset_1": "Изтриване на съобщения с потенциално опасни типове файлове", + "sieve_preset_2": "Винаги маркиране на имейла на определен изпращач като прочетен", + "sieve_preset_3": "Тихо изтриване, спиране на всякаква допълнителна обработка на sieve филтри", + "sieve_preset_4": "Архивиране на съобщението във ВХОДНА, пропускане на допълнителна обработка от sieve филтри", + "sieve_preset_5": "Автоотговор (ваканция)", + "sieve_preset_6": "Отхвърляне на съобщение с отговор", + "sieve_preset_7": "Пренасочване и запазване/изтриване", + "sieve_preset_8": "Пренасочване на имейл от определен изпращач, маркиране като прочетен и сортиране в подпапка", + "sieve_preset_header": "Моля, вижте примерните предефинирани набори по-долу. За повече детайли вижте Wikipedia.", + "sogo_visible": "Псевдонимът е видим в SOGo", + "sogo_visible_n": "Скриване на псевдоним в SOGo", + "sogo_visible_y": "Показване на псевдоним в SOGo", + "spam_aliases": "Времеви псевдоними", + "stats": "Статистика", + "status": "Статус", + "sync_jobs": "Синхронизиращи задачи", + "syncjob_check_log": "Проверка на лога", + "syncjob_last_run_result": "Резултат от последното изпълнение", + "syncjob_EX_OK": "Успех", + "syncjob_EXIT_CONNECTION_FAILURE": "Проблем с връзката", + "syncjob_EXIT_TLS_FAILURE": "Проблем с криптираната връзка", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Проблем с удостоверяването", + "syncjob_EXIT_OVERQUOTA": "Целевата пощенска кутия е над квотата", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не може да се свърже с отдалечен сървър", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Грешно потребителско име или парола", + "table_size": "Размер на таблицата", + "table_size_show_n": "Показване на %s елемента", + "target_address": "Адрес за пренасочване", + "target_domain": "Целеви домейн", + "templates": "Шаблони", + "template": "Шаблон", + "tls_enforce_in": "Принудително използване на TLS за входящи", + "tls_enforce_out": "Принудително използване на TLS за изходящи", + "tls_map_dest": "Дестинация", + "tls_map_dest_info": "Примери: example.org, .example.org, [mail.example.org]:25", + "tls_map_parameters": "Параметри", + "tls_map_parameters_info": "Празно или параметри, например: protocols=!SSLv2 ciphers=medium exclude=3DES", + "tls_map_policy": "Политика", + "tls_policy_maps": "Пренареждания на политиката на TLS", + "tls_policy_maps_enforced_tls": "Тези политики ще пренареждат и поведението за потребители на пощенски кутии, които принудително използват изходящи TLS връзки. Ако няма политика, съществуваща по-долу, тези потребители ще приложат стойностите по подразбиране, зададени като smtp_tls_mandatory_protocols и smtp_tls_mandatory_ciphers.", + "tls_policy_maps_info": "Тази политика за пренареждане на TLS транспортни правила е независима от настройките за политика на TLS на потребителя.
    \r\n Моля, вижте документацията за \"smtp_tls_policy_maps\" за повече информация.", + "tls_policy_maps_long": "Пренареждания на политиката на TLS за изходящи", + "toggle_all": "Превключване на всички", + "username": "Потребителско име", + "waiting": "Изчакване", + "weekly": "Седмично", + "yes": "✓" + }, + "oauth2": { + "access_denied": "Моля, влезте като собственик на пощенска кутия, за да удостоверите чрез OAuth2.", + "authorize_app": "Удостоверяване на приложение", + "deny": "Отказ", + "permit": "Удостоверяване на приложение", + "profile": "Профил", + "profile_desc": "Преглед на лични данни: потребителско име, пълно име, създадено, модифицирано, активно", + "scope_ask_permission": "Приложението заявява следните разрешения" + }, + "quarantine": { + "action": "Действие", + "atts": "Прикачени файлове", + "check_hash": "Търсене на файлов хеш в VirusTotal", + "confirm": "Потвърждаване", + "confirm_delete": "Потвърждаване на изтриването на този елемент.", + "danger": "Опасност", + "deliver_inbox": "Доставяне в пощенската кутия", + "disabled_by_config": "Текущата системна конфигурация деактивира функционалността на карантината. Моля, задайте \"задържания за пощенска кутия\" и \"максимален размер\" за елементите на карантината.", + "download_eml": "Изтегляне (.eml)", + "empty": "Няма резултати", + "high_danger": "Висока", + "info": "Информация", + "junk_folder": "Папка за нежелана поща", + "learn_spam_delete": "Учен като спам и изтриване", + "low_danger": "Ниска", + "medium_danger": "Средна", + "neutral_danger": "Неутрална", + "notified": "Уведомен", + "qhandler_success": "Заявката е изпратена успешно до системата. Можете да затворите прозореца сега.", + "qid": "QID на Rspamd", + "qinfo": "Системата за карантина ще запази отхвърлените съобщения в базата данни (изпращачът няма да бъде информиран за доставеното съобщение) както и съобщенията, доставени като копие в папката за нежелана поща на пощенска кутия.

    \"Учен като спам и изтриване\" ще научи съобщението като спам чрез теоремата на Бейс и също ще изчисли размити хешове, за да отхвърли подобни съобщения в бъдеще.

    Моля, имайте предвид, че обучаването на множество съобщения може да бъде - в зависимост от вашата система - времеемко.
    Елементите в черния списък са изключени от карантината.", + "qitem": "Елемент в карантина", + "quarantine": "Карантина", + "quick_actions": "Действия", + "quick_delete_link": "Отваряне на бърз линк за изтриване", + "quick_info_link": "Отваряне на бърз информационен линк", + "quick_release_link": "Отваряне на бърз линк за освобождане", + "rcpt": "Получател", + "received": "Получено", + "recipients": "Получатели", + "refresh": "Опресняване", + "rejected": "Отхвърлено", + "release": "Освобождане", + "release_body": "Прикаченото съобщение е добавено към това съобщение.", + "release_subject": "Потенциално вреден елемент в карантина %s", + "remove": "Премахване", + "rewrite_subject": "Презаписване на темата", + "rspamd_result": "Резултат от Rspamd", + "sender": "Изпращач (SMTP)", + "sender_header": "Изпращач (\"From\" заглавие)", + "settings_info": "Максимален брой елементи за карантиниране: %s
    Максимален размер на имейл: %s MiB", + "show_item": "Показване на елемент", + "spam": "Спам", + "spam_score": "Резултат", + "subj": "Тема", + "table_size": "Размер на таблицата", + "table_size_show_n": "Показване на %s елемента", + "text_from_html_content": "Съдържание (конвертиран HTML)", + "text_plain_content": "Съдържание (обикновен текст)", + "toggle_all": "Превключване на всички", + "type": "Тип" + }, + "queue": { + "delete": "Изтриване на всички", + "flush": "Изчистване на опашката", + "info": "Опашката на имейлите съдържа всички имейли, които чакат за доставка. Ако имейлът е засечен в опашката за дълго време, той автоматично се изтрива от системата.
    Съобщението за грешка на съответния имейл дава информация защо имейлът не може да бъде доставен.", + "legend": "Функции на действията в опашката на имейлите:", + "ays": "Моля, потвърдете, че искате да изтриете всички елементи от текущата опашка.", + "deliver_mail": "Доставяне", + "deliver_mail_legend": "Опит за повторно доставяне на избраните съобщения.", + "hold_mail": "Задържане", + "hold_mail_legend": "Задържа избраните съобщения. (Предотвратява допълнителни опити за доставяне)", + "queue_manager": "Мениджър на опашката", + "show_message": "Показване на съобщение", + "unban": "разблокиране на опашката", + "unhold_mail": "Освобождаване", + "unhold_mail_legend": "Освобождава избраните съобщения за доставка. (Изисква предварително задържане)" + }, + "ratelimit": { + "disabled": "Деактивирано", + "second": "съобщения/секунда", + "minute": "съобщения/минута", + "hour": "съобщения/час", + "day": "съобщения/ден" + }, + "start": { + "help": "Показване/Скриване на панела за помощ", + "imap_smtp_server_auth_info": "Моля, използвайте пълния си имейл адрес и механизма за удостоверяване PLAIN.
    \r\nВашите данни за вход ще бъдат криптирани от сървърната страна чрез задължително сървърно криптиране." + }, + "success": { + "acl_saved": "ACL за обект %s е запазен", + "admin_added": "Администраторът %s е добавен", + "admin_api_modified": "Промените в API са запазени", + "admin_modified": "Промените в администратора са запазени", + "admin_removed": "Администраторът %s е премахнат", + "alias_added": "Адресът на псевдонима %s (%d) е добавен", + "alias_domain_removed": "Домейнът на псевдонима %s е премахнат", + "alias_modified": "Промените в адреса на псевдонима %s са запазени", + "alias_removed": "Псевдонимът %s е премахнат", + "aliasd_added": "Добавен домейн на псевдоним %s", + "aliasd_modified": "Промените в домейна на псевдоним %s са запазени", + "app_links": "Запазени промени в линковете на приложенията", + "app_passwd_added": "Добавена нова парола за приложение", + "app_passwd_removed": "Премахната парола за приложение с ID %s", + "bcc_deleted": "Записи в картата BCC са изтрити: %s", + "bcc_edited": "Записът в картата BCC %s е редактиран", + "bcc_saved": "Записът в картата BCC е запазен", + "cors_headers_edited": "Настройките на CORS са запазени", + "db_init_complete": "Инициализацията на базата данни е завършена", + "delete_filter": "Изтрити филтри с ID %s", + "delete_filters": "Изтрити филтри: %s", + "deleted_syncjob": "Изтрита синхронизираща задача с ID %s", + "deleted_syncjobs": "Изтрити синхронизиращи задачи: %s", + "dkim_added": "Добавен DKIM ключ %s", + "dkim_duplicated": "DKIM ключът за домейн %s е копиран в %s", + "dkim_removed": "DKIM ключът %s е премахнат", + "domain_add_dkim_available": "DKIM ключът вече съществува", + "domain_added": "Добавен домейн %s", + "domain_admin_added": "Добавен администратор на домейн %s", + "domain_admin_modified": "Промените в администратора на домейн %s са запазени", + "domain_admin_removed": "Администраторът на домейн %s е премахнат", + "domain_footer_modified": "Промените в подвала на домейн %s са запазени", + "domain_modified": "Промените в домейн %s са запазени", + "domain_removed": "Домейнът %s е премахнат", + "dovecot_restart_success": "Dovecot е рестартиран успешно", + "eas_reset": "Устройствата с ActiveSync за потребителя %s са нулирани", + "f2b_banlist_refreshed": "Списъкът с ID на блокираните е актуализиран успешно.", + "f2b_modified": "Промените в параметрите на Fail2ban са запазени", + "forwarding_host_added": "Добавен хост за препращане %s", + "forwarding_host_removed": "Премахнат хост за препращане %s", + "global_filter_written": "Филтърът е записан успешно във файл", + "hash_deleted": "Хешът е изтрит", + "iam_test_connection": "Връзката е успешна", + "ip_check_opt_in_modified": "Проверката на IP беше успешно запазена", + "item_deleted": "Елементът %s е изтрит успешно", + "item_released": "Елементът %s е освободен", + "items_deleted": "Елементът %s е изтрит успешно", + "items_released": "Избраните елементи са освободени", + "learned_ham": "Успешно научен ID %s като не е спам", + "license_modified": "Промените в лиценза са запазени", + "logged_in_as": "Вписан като %s", + "mailbox_added": "Пощенската кутия %s е добавена", + "mailbox_modified": "Промените в пощенската кутия %s са запазени", + "mailbox_removed": "Пощенската кутия %s е премахната", + "mailbox_renamed": "Пощенската кутия е преименувана от %s на %s", + "nginx_reloaded": "Nginx е презареден", + "object_modified": "Промените в обект %s са запазени", + "password_changed_success": "Паролата е променена успешно", + "password_policy_saved": "Политиката за парола е запазена успешно", + "pushover_settings_edited": "Настройките на Pushover са запазени успешно, моля, проверете удостоверенията.", + "qlearn_spam": "Съобщението с ID %s е научено като спам и изтрито", + "queue_command_success": "Командата на опашката е завършена успешно", + "recipient_map_entry_deleted": "Записът в картата на получател с ID %s е изтрит", + "recipient_map_entry_saved": "Записът в картата на получател \"%s\" е запазен", + "recovery_email_sent": "Изпратен имейл за възстановяване до %s", + "relayhost_added": "Записът в картата %s е добавен", + "relayhost_removed": "Записът в картата %s е премахнат", + "reset_main_logo": "Нулиране на логото по подразбиране", + "resource_added": "Ресурсът %s е добавен", + "resource_modified": "Промените в пощенската кутия %s са запазени", + "resource_removed": "Ресурсът %s е премахнат", + "rl_saved": "Ограничението на скоростта за обект %s е запазено", + "rspamd_ui_pw_set": "Паролата на UI на Rspamd е зададена успешно", + "saved_settings": "Запазени настройки", + "settings_map_added": "Добавен запис в картата с настройки", + "settings_map_removed": "Премахнат запис в картата с настройки с ID %s", + "sogo_profile_reset": "Профилът на SOGo за потребителя %s е нулиран", + "template_added": "Добавен шаблон %s", + "template_modified": "Промените в шаблон %s са запазени", + "template_removed": "Шаблонът с ID %s е премахнат", + "tls_policy_map_entry_deleted": "Записът в картата на политиката на TLS с ID %s е изтрит", + "tls_policy_map_entry_saved": "Записът в картата на политиката на TLS \"%s\" е запазен", + "ui_texts": "Запазени промени в текстовете на UI", + "upload_success": "Файлът е качен успешно", + "verified_fido2_login": "Потвърдено вход с FIDO2", + "verified_totp_login": "Потвърдено вход с TOTP", + "verified_webauthn_login": "Потвърдено вход с WebAuthn", + "verified_yotp_login": "Потвърдено вход с Yubico OTP" + }, + "tfa": { + "authenticators": "Аутентикатори", + "api_register": "%s използва облачното API на Yubico. Моля, вземете API ключ за вашия ключ тук", + "confirm": "Потвърждаване", + "confirm_totp_token": "Моля, потвърдете промените, като въведете генерирания токен", + "delete_tfa": "Деактивиране на TFA", + "disable_tfa": "Деактивиране на TFA до следващия успешен вход", + "enter_qr_code": "Вашият TOTP код, ако вашето устройство не може да сканира QR кодове", + "error_code": "Код на грешка", + "init_webauthn": "Инициализиране, моля, изчакайте...", + "key_id": "Идентификатор за вашето устройство", + "key_id_totp": "Идентификатор за вашия ключ", + "none": "Деактивиране", + "reload_retry": "- (презаредете браузъра, ако грешката продължи)", + "scan_qr_code": "Моля, сканирайте следния код с вашето приложение за аутентикация или въведете кода ръчно.", + "select": "Моля, изберете", + "set_tfa": "Задаване на метод за двуфакторно удостоверяване", + "start_webauthn_validation": "Стартиране на проверка", + "tfa": "Двуфакторно удостоверяване", + "tfa_token_invalid": "Невалиден токен за TFA", + "totp": "Временен OTP (Google Authenticator, Authy и др.)", + "u2f_deprecated": "Изглежда, че вашият ключ е регистриран с депрекирания метод U2F. Ще деактивираме двуфакторното удостоверяване за вас и ще изтрием вашия ключ.", + "u2f_deprecated_important": "Моля, регистрирайте вашия ключ в административния панел с новия метод WebAuthn.", + "webauthn": "Удостоверяване с WebAuthn", + "waiting_usb_auth": "Изчакване на USB устройство...

    Моля, докоснете бутона на вашето USB устройство сега.", + "waiting_usb_register": "Изчакване на USB устройство...

    Моля, въведете паролата си по-горе и потвърдете регистрацията си, като докоснете бутона на вашето USB устройство.", + "yubi_otp": "Удостоверяване с Yubico OTP" + }, + "user": { + "action": "Действие", + "active": "Активен", + "active_sieve": "Активен филтър", + "advanced_settings": "Разширени настройки", + "alias": "Псевдоним", + "alias_create_random": "Генериране на случаен псевдоним", + "alias_extend_all": "Удължаване на всички псевдоними с 1 час", + "alias_full_date": "d.m.Y, H:i:s T", + "alias_remove_all": "Премахване на всички псевдоними", + "alias_select_validity": "Период на валидност", + "alias_time_left": "Оставащо време", + "alias_valid_until": "Валиден до", + "aliases_also_send_as": "Разрешено да изпраща и като потребител", + "aliases_send_as_all": "Не проверявай достъпа на изпращач за следните домейни и техните псевдоними", + "allowed_protocols": "Разрешени протоколи", + "app_hint": "Паролите за приложения са алтернативни пароли за вашия IMAP, SMTP, CalDAV, CardDAV и EAS вход. Потребителското име остава непроменено. SOGo уеб пощата не е достъпна чрез пароли за приложения.", + "app_name": "Име на приложението", + "app_passwds": "Пароли за приложения", + "apple_connection_profile": "Профил за връзка на Apple", + "apple_connection_profile_complete": "Този профил за връзка включва настройки за IMAP и SMTP, както и пътища за CalDAV (календари) и CardDAV (контакти) за устройство на Apple.", + "apple_connection_profile_mailonly": "Този профил за връзка включва настройки за IMAP и SMTP за устройство на Apple.", + "apple_connection_profile_with_app_password": "Генерирана е нова парола за приложение и е добавена към профила, така че не е необходимо да се въвежда парола при настройката на вашето устройство. Моля, не споделяйте файла, тъй като той предоставя пълен достъп до вашата пощенска кутия.", + "attribute": "Атрибут", + "authentication": "Удостоверяване", + "change_password": "Промяна на парола", + "change_password_hint_app_passwords": "Вашият акаунт има %d пароли за приложения, които няма да бъдат променени. За управление на тях, отидете в раздела \"Пароли за приложения\".", + "clear_recent_successful_connections": "Изчистване на видимите успешни връзки", + "client_configuration": "Показване на ръководства за настройка на пощенски клиенти и смартфони", + "create_app_passwd": "Създаване на парола за приложение", + "create_syncjob": "Създаване на нова синхронизираща задача", + "created_on": "Създаден на", + "daily": "Ежедневно", + "day": "ден", + "delete_ays": "Моля, потвърдете процеса на изтриване.", + "description": "Описание", + "direct_aliases": "Директни адреси на псевдоними", + "direct_aliases_desc": "Директните адреси на псевдоними се засягат от настройките за филтър за спам и политика за TLS.", + "direct_protocol_access": "Този потребител на пощенска кутия има директен, външен достъп до следните протоколи и приложения. Тази настройка се контролира от вашия администратор. Паролите за приложения могат да бъдат създадени, за да се предостави достъп до отделни протоколи и приложения.
    Бутонът \"Уеб поща\" предоставя еднократно удостоверяване към SOGo и винаги е достъпен.", + "eas_reset": "Нулиране на кеша на устройствата с ActiveSync", + "eas_reset_help": "В много случаи нулирането на кеша на устройството ще помогне за възстановяването на повреден профил на ActiveSync.
    Внимание: Всички елементи ще бъдат презаредени!", + "eas_reset_now": "Нулиране сега", + "edit": "Редактиране", + "email": "Имейл", + "email_and_dav": "Имейл, календари и контакти", + "empty": "Няма резултати", + "encryption": "Криптиране", + "excludes": "Изключване", + "expire_in": "Изтича след", + "fido2_webauthn": "FIDO2/WebAuthn", + "force_pw_update": "Трябва да зададете нова парола, за да имате достъп до груповите услуги.", + "from": "от", + "generate": "генериране", + "hour": "час", + "hourly": "Часово", + "hours": "часа", + "in_use": "В употреба", + "interval": "Интервал", + "is_catch_all": "Catch-all за домейн/и", + "last_mail_login": "Последен вход в пощата", + "last_pw_change": "Последна промяна на парола", + "last_run": "Последно изпълнение", + "last_ui_login": "Последен вход в UI", + "loading": "Зареждане...", + "login_history": "История на входовете", + "mailbox": "Пощенска кутия", + "mailbox_details": "Детайли", + "mailbox_general": "Общи", + "mailbox_settings": "Настройки", + "messages": "съобщения", + "month": "месец", + "months": "месеца", + "never": "Никога", + "new_password": "Нова парола", + "new_password_repeat": "Потвърждаване на новата парола", + "no_active_filter": "Няма активен филтър", + "no_last_login": "Няма информация за последен вход в UI", + "no_record": "Няма запис", + "open_logs": "Отваряне на логове", + "open_webmail_sso": "Уеб поща", + "overview": "Преглед", + "password": "Парола", + "password_now": "Текуща парола (потвърждаване на промените)", + "password_repeat": "Парола (повторете)", + "password_reset_info": "Ако не е зададен имейл за възстановяване на парола, тази функция не може да бъде използвана.", + "protocols": "Протоколи", + "pushover_evaluate_x_prio": "Ескалиране на високоприоритетни съобщения [X-Priority: 1]", + "pushover_info": "Настройките за известия на Pushover ще се прилагат за всички чисти (не-спам) съобщения, доставени до %s, включително псевдоними (споделени, несподелени, таговани).", + "pushover_only_x_prio": "Разглеждане само на високоприоритетни съобщения [X-Priority: 1]", + "pushover_sender_array": "Разглеждане на следните адреси на изпращач (разделени с запетая)", + "pushover_sender_regex": "Съвпадение на изпращач с следния regex", + "pushover_sound": "Звук", + "pushover_text": "Текст на известието", + "pushover_title": "Заглавие на известието", + "pushover_vars": "Когато не е зададен филтър на изпращач, всички съобщения ще бъдат разглеждани.
    Филтрите с регулярен израз, както и точните проверки на изпращач, могат да бъдат дефинирани индивидуално и ще бъдат разглеждани последователно. Те не зависят един от друг.
    Използвайте следните променливи за текст и заглавие (моля, вземете предвид политиките за поверителност на данните)", + "pushover_verify": "Проверка на удостоверения", + "pw_recovery_email": "Имейл за възстановяване на парола", + "q_add_header": "Папка за нежелана поща", + "q_all": "Всички категории", + "q_reject": "Отхвърлени", + "quarantine_category": "Категория на уведомленията за карантина", + "quarantine_category_info": "Категорията \"Отхвърлени\" включва съобщения, които са били отхвърлени, докато категорията \"Папка за нежелана поща\" ще уведоми потребителя за съобщения, които са били поставени в папката за нежелана поща.", + "quarantine_notification": "Уведомления за карантина", + "quarantine_notification_info": "След като уведомлението е изпратено, елементите ще бъдат маркирани като \"уведомени\" и няма да бъдат изпращани допълнителни уведомления за този конкретен елемент.", + "recent_successful_connections": "Видими успешни връзки", + "remove": "Премахване", + "running": "Изпълнява се", + "save": "Запазване на промените", + "save_changes": "Запазване на промените", + "sender_acl_disabled": "Проверката на изпращач е деактивирана", + "shared_aliases": "Споделени адреси на псевдоними", + "shared_aliases_desc": "Споделените псевдоними не се засягат от настройките за филтър за спам и политика за TLS на потребителя. Съответните настройки за спам филтър могат да бъдат направени само от администратор като настройка за домейн.", + "show_sieve_filters": "Показване на активния потребителски филтър на sieve", + "sogo_profile_reset": "Нулиране на профила на SOGo", + "sogo_profile_reset_help": "Това ще унищожи профила на SOGo и ще изтрие всички данни за контакти и календар безвъзвратно.", + "sogo_profile_reset_now": "Нулиране сега", + "spam_aliases": "Временни адреси на псевдоними", + "spam_score_reset": "Нулиране до настройките на сървъра по подразбиране", + "spamfilter": "Филтър за спам", + "spamfilter_behavior": "Рейтинг", + "spamfilter_bl": "Черен списък", + "spamfilter_bl_desc": "Адресите в черния списък винаги ще бъдат класифицирани като спам и отхвърлени. Отхвърлените съобщения не ще бъдат копирани в карантина. Поддържат се уайлдкардове.
    Отхвърлените съобщения няма да бъдат доставени до папката за нежелана поща.", + "spamfilter_default_score": "Стойности по подразбиране", + "spamfilter_green": "Зелено: това съобщение не е спам", + "spamfilter_hint": "Първата стойност описва \"нисък резултат за спам\", втората представлява \"висок резултат за спам\".", + "spamfilter_red": "Червено: това съобщение е спам и ще бъде отхвърлено от сървъра", + "spamfilter_table_action": "Действие", + "spamfilter_table_add": "Добавяне на елемент", + "spamfilter_table_domain_policy": "политика на домейна", + "spamfilter_table_empty": "Няма данни за показване", + "spamfilter_table_remove": "премахване", + "spamfilter_table_rule": "Правило", + "spamfilter_wl": "Бял списък", + "spamfilter_wl_desc": "Адресите в белия списък никога ще бъдат класифицирани като спам. Поддържат се уайлдкардове.
    Съобщенията, които не са класифицирани като спам, ще бъдат доставени в папката за нежелана поща, ако резултатът за спам е над стойността за висок резултат за спам.", + "spamfilter_yellow": "Жълто: това съобщение може да е спам, ще бъде маркирано като спам и преместено в папката за нежелана поща", + "status": "Статус", + "sync_jobs": "Синхронизиращи задачи", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Проблем с удостоверяването", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Грешно потребителско име или парола", + "syncjob_EXIT_CONNECTION_FAILURE": "Проблем с връзката", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Не може да се свърже с отдалечен сървър", + "syncjob_EXIT_OVERQUOTA": "Целевата пощенска кутия е над квотата", + "syncjob_EXIT_TLS_FAILURE": "Проблем с криптираната връзка", + "syncjob_EX_OK": "Успех", + "syncjob_check_log": "Проверка на лога", + "syncjob_last_run_result": "Резултат от последното изпълнение", + "tag_handling": "Настройка за тагове", + "tag_help_example": "Пример за тагован имейл адрес: me+Facebook@example.org", + "tag_help_explain": "В подпапка: ще бъде създадена нова подпапка под INBOX (\"INBOX/Facebook\").
    \r\nВ тема: първото име на тага ще бъде добавено към темата на съобщението, например: \"[Facebook] Моите новини\".", + "tag_in_none": "Не прави нищо", + "tag_in_subfolder": "В подпапка", + "tag_in_subject": "В тема", + "text": "Текст", + "tfa_info": "Двуфакторното удостоверяване помага за защита на вашия акаунт. Ако го активирате, ще ви трябват пароли за приложения, за да влезете в приложения или услуги, които не поддържат двуфакторно удостоверяване (напр. пощенски клиенти).", + "title": "Заглавие", + "tls_enforce_in": "Принудително използване на TLS за входящи", + "tls_enforce_out": "Принудително използване на TLS за изходящи", + "tls_policy": "Политика за криптиране", + "tls_policy_warning": "Внимание: Ако решите да принудите преноса на криптирани съобщения, може да загубите съобщения.
    Съобщенията, които не удовлетворяват политиката, ще бъдат върнати с твърдо отхвърляне от системата за поща.
    Тази опция засяга вашия основен адрес на пощенска кутия (логин име), всички адреси, произтичащи от домейни на псевдоними, както и адреси на псевдоними, с единствена целева пощенска кутия.", + "user_settings": "Настройки на потребителя", + "username": "Потребителско име", + "value": "Стойност", + "verify": "Проверка", + "waiting": "Изчакване", + "week": "седмица", + "weekly": "Weekly", + "weeks": "седмици", + "with_app_password": "с парола за приложение", + "year": "година", + "years": "години" + }, + "warning": { + "cannot_delete_self": "Не може да изтриете влезлия потребител", + "domain_added_sogo_failed": "Добавен домейн, но неуспешно рестартиране на SOGo, моля, проверете системните логове.", + "dovecot_restart_failed": "Dovecot не успя да рестартира, моля, проверете логовете", + "fuzzy_learn_error": "Грешка при обучение на размит хеш: %s", + "hash_not_found": "Хешът не е намерен или вече е изтрит", + "ip_invalid": "Пропуснат невалиден IP: %s", + "is_not_primary_alias": "Пропуснат неосновен псевдоним %s", + "no_active_admin": "Не може да деактивирате последния активен администратор", + "quota_exceeded_scope": "Квотата на домейна е надвишена: Само неограничени пощенски кутии могат да бъдат създадени в този домейн.", + "session_token": "Невалиден формулярен токен: Несъответствие на токена.", + "session_ua": "Невалиден формулярен токен: Грешка при проверка на User-Agent." + } +} diff --git a/mailcow/data/web/lang/lang.ca-es.json b/mailcow/data/web/lang/lang.ca-es.json index af7cbd5..5b8e9b6 100644 --- a/mailcow/data/web/lang/lang.ca-es.json +++ b/mailcow/data/web/lang/lang.ca-es.json @@ -19,7 +19,16 @@ "spam_alias": "Àlies temporals", "spam_score": "Puntuació de correu brossa", "tls_policy": "Política TLS", - "unlimited_quota": "Quota ilimitada per bústies de correo" + "unlimited_quota": "Quota ilimitada per bústies de correo", + "delimiter_action": "Acció delimitadora", + "domain_relayhost": "Canviar relayhost per un domini", + "extend_sender_acl": "Permetre extendre l'ACL del remitent per adreces externes", + "mailbox_relayhost": "Canvia el host de reenviament per una bústia", + "pushover": "Pushover", + "pw_reset": "Permetre el restabliment de la contrasenya de l'usuari mailcow", + "ratelimit": "Límit de peticions", + "smtp_ip_access": "Canvia hosts permesos per SMTP", + "sogo_access": "Permetre la gestió d'accés a SOGo" }, "add": { "activate_filter_warn": "All other filters will be deactivated, when active is checked.", @@ -73,7 +82,25 @@ "validate": "Validar", "validation_success": "Validated successfully", "app_name": "Nom de l'aplicació", - "app_password": "Afegir contrasenya a l'aplicació" + "app_password": "Afegir contrasenya a l'aplicació", + "app_passwd_protocols": "Protocols autoritzats per la contrasenya de l'aplicació", + "bcc_dest_format": "La destinació c/o ha de ser una única adreça de correu vàlida.
    Si necessiteu enviar una còpia a diverses adreces, creeu un àlies i utilitzeu-lo aquí.", + "comment_info": "Els comentaris privats no són visibles per l'usuari, mentre que els comentaris públics apareixen com una descripció emergent a la informació de l'usuari", + "custom_params": "Paràmetres personalitzats", + "custom_params_hint": "Correcte: --param=xy, incorrecte: --param xy", + "destination": "Destí", + "disable_login": "No permetre l'inici de sessió (els missatges entrants continuen sent acceptats)", + "domain_matches_hostname": "El domini %s coincideix amb el nom del servidor", + "dry": "Simular la sincronització", + "gal": "Llista d'adreces global", + "generate": "genereu", + "inactive": "Inactiu", + "internal": "Intern", + "internal_info": "Els àlies interns són només accessibles des del mateix domini o els àlies de dominis.", + "mailbox_quota_def": "Quota per defecte de la bústia", + "nexthop": "Següent salt", + "private_comment": "Comentari privat", + "public_comment": "Comentari púlbic" }, "admin": { "access": "Accés", @@ -557,4 +584,4 @@ "week": "Setmana", "weeks": "Setmanes" } -} \ No newline at end of file +} diff --git a/mailcow/data/web/lang/lang.cs-cz.json b/mailcow/data/web/lang/lang.cs-cz.json index 67eb52b..b178720 100644 --- a/mailcow/data/web/lang/lang.cs-cz.json +++ b/mailcow/data/web/lang/lang.cs-cz.json @@ -1,7 +1,7 @@ { "acl": { "alias_domains": "Doménové aliasy", - "app_passwds": "Hesla aplikací", + "app_passwds": "Správa hesel aplikací", "bcc_maps": "BCC mapy", "delimiter_action": "Zacházení s označkovanou poštou", "domain_desc": "Změnit popis domény", @@ -24,7 +24,7 @@ "sogo_access": "Správa přístupu do SOGo", "sogo_profile_reset": "Resetování profilu SOGo", "spam_alias": "Dočasné aliasy", - "spam_policy": "Blacklist/Whitelist", + "spam_policy": "Denylist/Allowlist", "spam_score": "Skóre spamu", "syncjobs": "Synchronizační úlohy", "tls_policy": "Pravidla TLS", @@ -82,12 +82,12 @@ "password": "Heslo", "password_repeat": "Potvrzení nového hesla (opakujte)", "port": "Port", - "post_domain_add": "Po přidání nové domény je nutné restartovat SOGo kontejner!", + "post_domain_add": "Po přidání nové domény se musí restartovat kontejner SOGo!

    Je také třeba ověřit nastavení DNS nové domény. Po ověření restartujte kontejner \"acme-mailcow\", aby se vygenerovaly certifikáty domény (autoconfig.<domain>, autodiscover.<domain>).
    Tento krok je volitelný, a provede se automaticky každých 24 hodin.", "private_comment": "Soukromý komentář", "public_comment": "Veřejný komentář", "quota_mb": "Kvóta (MiB)", "relay_all": "Předávání všech příjemců", - "relay_all_info": "Pokud se rozhodnete nepředávat všechny příjemce, musíte přidat prázdnou mailovou schránku pro každého příjemce, který se má předávat.", + "relay_all_info": "↪Pokud se rozhodnete nepředávat všechny příjemce, musíte přidat prázdnou mailovou schránku pro každého příjemce, který se má předávat.", "relay_domain": "Předávání domény", "relay_transport_info": "
    Info
    U této domény lze pro konkrétní cíl nastavit transportní mapu. Není-li nastavena, použije se MX záznam.", "relay_unknown_only": "Předávat jen neexistující schránky. Doručení do existujících proběhne lokálně.", @@ -109,7 +109,9 @@ "validate": "Ověřit", "validation_success": "Úspěšně ověřeno", "tags": "Štítky", - "dry": "Simulovat synchronizaci" + "dry": "Simulovat synchronizaci", + "internal": "Interní", + "internal_info": "Interní aliasy jsou přístupné jen z vlastních domén nebo jejich aliasů." }, "admin": { "access": "Přístupy", @@ -147,7 +149,7 @@ "arrival_time": "Čas zařazení do fronty (čas na serveru)", "authed_user": "Přihlášený uživatel", "ays": "Opravdu chcete pokračovat?", - "ban_list_info": "Seznam blokovaných IP adres je zobrazen níže: síť (zbývající čas blokování) - [akce].
    IP adresy zařazené pro odblokování budou z aktivního seznamu odebrány během několika sekund.
    Červeně označené položky jsou pernamentní bloky z blacklistu.", + "ban_list_info": "Viz seznam zablokovaných IP níže: síť (zbývající doba zablokování) - [akce].
    IP adresy zařazené pro odblokování budou z aktivního seznamu odebrány během pár sekund.
    Červeně označeny jsou položky z trvalých seznamů.", "change_logo": "Změnit logo", "logo_normal_label": "Normální", "logo_dark_label": "Inverzní pro tmavý režim", @@ -181,16 +183,16 @@ "empty": "Žádné výsledky", "excludes": "Vyloučit tyto příjemce", "f2b_ban_time": "Doba blokování (s)", - "f2b_blacklist": "Sítě/hostitelé na blacklistu", + "f2b_blacklist": "Sítě či hostitelé na seznamu zákazů", "f2b_filter": "Regex filtre", - "f2b_list_info": "Síť nebo hostitelé na blacklistu mají vždy větší váhu než položky na whitelistu. Blacklist se sestavuje vždy při startu kontejneru.", + "f2b_list_info": "Sítě či hostitelé na seznamu zákazů mají vždy větší váhu než položky na seznamu povolení. Každá úprava seznamu trvá pár sekund.", "f2b_max_attempts": "Max. pokusů", "f2b_netban_ipv4": "Rozsah IPv4 podsítě k zablokování (8-32)", "f2b_netban_ipv6": "Rozsah IPv6 podsítě k zablokování (8-128)", "f2b_parameters": "Parametry automatického firewallu", "f2b_regex_info": "Záznamy které se berou v úvahu: SOGo, Postfix, Dovecot, PHP-FPM.", "f2b_retry_window": "Časový horizont pro maximum pokusů (s)", - "f2b_whitelist": "Sítě/hostitelé na whitelistu", + "f2b_whitelist": "Sítě či hostitelé na seznamu povolení", "filter_table": "Tabulka filtrů", "forwarding_hosts": "Předávající servery", "forwarding_hosts_add_hint": "Lze zadat IPv4/IPv6 adresy, sítě ve formátu CIDR, názvy serverů (budou převedeny na IP adresy) nebo názvy domén (budou převedeny na IP pomocí SPF záznamů, příp. MX záznamů).", @@ -256,7 +258,7 @@ "quarantine_exclude_domains": "Vyloučené domény a doménové aliasy", "quarantine_max_age": "Maximální stáří ve dnech
    Hodnota musí být rovna nebo větší než 1 den.", "quarantine_max_score": "Neposílat notifikace pokud je spam skóre větší než hodnota:
    Výchozí je 9999.0", - "quarantine_max_size": "Maximální velikost v MiB (větší prvky budou smazány)
    0 neznamená neomezeno.", + "quarantine_max_size": "Maximální velikost v MiB (větší prvky budou smazány)
    0 neznamená neomezeno.", "quarantine_notification_html": "Šablona upozornění:
    Ponechte prázdné, aby se obnovila výchozí šablona.", "quarantine_notification_sender": "Odesílatel upozornění", "quarantine_notification_subject": "Předmět upozornění", @@ -264,7 +266,7 @@ "quarantine_release_format": "Formát propuštěných položek", "quarantine_release_format_att": "Jako příloha", "quarantine_release_format_raw": "Nezměněný originál", - "quarantine_retention_size": "Počet zadržených zpráv na mailovou schránku
    0 znamená neaktivní.", + "quarantine_retention_size": "Počet zadržených zpráv na mailovou schránku
    0 znamená neaktivní.", "quota_notification_html": "Šablona upozornění:
    Ponechte prázdné, aby se obnovila výchozí šablona.", "quota_notification_sender": "Odesílatel upozornění", "quota_notification_subject": "Předmět upozornění", @@ -283,7 +285,7 @@ "relay_rcpt": "\"Komu:\" adresa", "relay_run": "Provést test", "relayhosts": "Transporty podle odesílatele", - "relayhosts_hint": "Zde definujte transporty podle odesílatele, jež pak můžete použít v nastavení domény.
    \r\nProtokol transportu je vždy \"smtp:\". Bere se v potaz uživatelské nastavení odchozího TLS.", + "relayhosts_hint": "Zde definujte transporty podle odesílatele, jež pak můžete použít v nastavení domény.
    \nProtokol transportu je vždy \"smtp:\" a použije se TLS, je-li nabídnuto. Zabalené TLS (SMTPS) se nepodporuje. Bere se v potaz uživatelské nastavení odchozího TLS.
    \nTýká se vybraných domén včetně doménových aliasů.", "remove": "Smazat", "remove_row": "Smazat řádek", "reset_default": "Obnovit výchozí nastavení", @@ -299,11 +301,11 @@ "rsettings_preset_2": "Postmasteři chtějí dostávat spam", "rsettings_preset_3": "Povolit jen určité odesílatele pro schránku (např. jen interní schránka)", "rsettings_preset_4": "Deaktivujte Rspamd pro doménu", - "rspamd_com_settings": "Rspamd dokumentace\r\n - Název nastavení bude automaticky vygenerován, viz níže uvedené předvolby.", + "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 jiné globální black- a whitelisty.", - "rspamd_global_filters_regex": "Názvy jsou dostatečným vysvětlením. Musí obsahovat jen platné regulární výrazy ve formátu \"/vyraz/parametry\" (e.g. /.+@domena\\.tld/i).
    \r\n Každý výraz bude podroben základní kontrole, přesto je možné Rspamd 'rozbít', nebude-li syntax zcela korektní.
    \r\n Rspamd se pokusí načíst mapu po každé změně. V případě potíží, restartujte Rspamd, aby se konfigurace načetla explicitně.", + "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'", "save": "Uložit změny", @@ -324,8 +326,8 @@ "to_top": "Zpět na začátek", "transport_dest_format": "Formát: example.org, .example.org, *, box@example.org (vícero položek lze oddělit čárkou)", "transport_maps": "Transportní mapy", - "transport_test_rcpt_info": "• Na otestování odchozí pošty je možné použít null@hosted.mailcow.de jako adresáta", - "transports_hint": "→ Položka transportní mapy přebíjí transportní mapu podle odesílatele.
    \r\n→ Uživatelské nastavení odchozího TLS se ignoruje a lze je výhradně vynutit mapováním TLS pravidel.
    \r\n→ Protokol transportu je vždy \"smtp:\".
    \r\n→ Adresy, jež odpovídají výrazu \"/localhost$/\", se vždy předají přes \"local:\", takže nejsou zahrnuty do definice cíle \"*\".
    \r\n→ Pro stanovení přihlašovacích údajů dalšího skoku, např. \"[host]:25\", bude Postfix vždy hledat nejdříve \"host\" a teprve pak \"[host]:25\". Kvůli tomu nelze použít současně \"host\" a \"[host]:25\"", + "transport_test_rcpt_info": "• K otestování předávání pošty ven použijte null@hosted.mailcow.de.", + "transports_hint": "• Položka transportní mapy přebíjí transportní mapu podle odesílatele.
    \n• Transporty založené na MX mají přednost.
    \n• Uživatelské nastavení odchozího TLS se ignoruje a lze je vynutit výhradně mapou TLS pravidel.
    \n• Transportní služnou pro tyto transporty je vždy \"smtp:\" a použije se TLS, je-li nabídnuto. Zabalené TLS (SMTPS) se nepodporuje.
    \n• Adresy, jež odpovídají výrazu \"/localhost$/\", se vždy předají přes \"local:\", takže nejsou zahrnuty do definice cíle \"*\".
    \n• Pro stanovení přihlašovacích údajů dalšího skoku, např. \"[host]:25\", bude Postfix vždy hledat nejdříve \"host\" a teprve pak \"[host]:25\". Kvůli tomu nelze použít současně \"host\" a \"[host]:25\".", "ui_footer": "Pata stránka (HTML povoleno)", "ui_header_announcement": "Oznámení", "ui_header_announcement_active": "Nastavit jako aktivní", @@ -344,17 +346,72 @@ "validate_license_now": "Ověřit GUID na licenčním serveru", "verify": "Ověřit", "yes": "✓", - "f2b_ban_time_increment": "Délka banu je prodlužována s každým dalším banem", - "f2b_max_ban_time": "Maximální délka banu (s)", + "f2b_ban_time_increment": "Délka bloku se prodlužuje s každým dalším zablokováním", + "f2b_max_ban_time": "Maximální délka bloku (s)", "cors_settings": "Nastavení CORS", - "queue_unban": "zrušit ban", + "queue_unban": "odblokovat", "password_reset_info": "Pokud není zadán žádný e-mail pro obnovení, nelze tuto funkci použít.", "password_reset_settings": "Nastavení obnovení hesla", "password_settings": "Nastavení hesel", "password_reset_tmpl_html": "HTML šablona", "password_reset_tmpl_text": "Textová šablona", "reset_password_vars": "{{link}} Vygenerovaný odkaz pro obnovení hesla
    {{username}} Název mailboxu uživatele, který požádal o resetování hesla.
    {{username2}} Název schránky pro obnovení
    {{date}} Datum podání žádosti o obnovení hesla
    {{token_lifetime}} Délka životnosti tokenu v minutách
    {{hostname}} Název serveru mailcow", - "restore_template": "Ponechte prázdné pro obnovení výchozí šablony." + "restore_template": "Ponechte prázdné pro obnovení výchozí šablony.", + "copy_to_clipboard": "Text zkopírován do schránky!", + "iam_login_provisioning": "Automaticky vytvořit uživatele při přihlášení", + "user_quicklink": "Skrýt zkratku na přihlášení uživatele", + "domainadmin_quicklink": "Skrýt zkratku na přihlášení správce domény", + "iam_auth_flow_info": "Kromě metody autorizačního kódu (Authorization Code Flow, výchozího v Keycloaku), jež se používá pro SSO, podporuje mailcow také metody autentizaci přímo pomocí přihlašovacích údajů. The metoda Mailpassword Flow se pokusí ověřit přihlašovací údaje uživatele přímo v Admin REST API Keycloaku. mailcow získá hash hesla z atributu mailcow_password , namapovaného v Keycloaku.", + "iam_default_template_description": "Nemá-li uživatel přiřazenu šablonu, použije se výchozí šablona k vytvoření schránky, ale ne k její úpravě či aktualizaci.", + "iam_userinfo_url": "Koncový bod pro informace o uživatelích", + "iam_redirect_url": "URL přesměrování", + "user_link": "Odkaz pro uživatele", + "force_sso_text": "Je-li nastaven externí poskytovatel OIDC, zapnutím této volby skryjete výchozí přihlašovací formulář. Zůstane vidět jen tlačítko pro SSO", + "iam_mapping": "Mapování atributů", + "iam_bindpass": "Heslo pro bind", + "iam_periodic_full_sync": "Pravidelná úplná synchronizace", + "iam_port": "Port", + "iam_realm": "Realm", + "iam_rest_flow": "Mailpassword Flow", + "iam_server_url": "URL serveru", + "iam_sso": "Single Sign-On", + "iam_sync_interval": "Interval synchronizace/importu (min)", + "iam_test_connection": "Test spojení", + "iam_token_url": "Koncový bod pro tokeny", + "iam_username_field": "Pole uživatelského jména", + "iam_binddn": "Doména pro bind", + "iam_use_ssl": "Používat SSL", + "iam_use_tls": "Používat StartTLS", + "iam_version": "Verze", + "quicklink_text": "Zobrazení zkratek k dalším přihlašovacím stránkám", + "iam_use_tls_info": "Je-li zapnuto TLS, musí se používat standardní port pro LDAP (389). Port SSL nelze použít.", + "ignore_ssl_error": "Ignorovat chyby SSL", + "iam_use_ssl_info": "Je-li zapnuto SSL a nastaven port 389, použije se automaticky port 636.", + "task": "Úloha", + "app_hide": "Skrýt při přihlášení", + "admin_quicklink": "Skrýt zkratku na přihlášení správce", + "allowed_methods": "Access-Control-Allow-Methods", + "allowed_origins": "Access-Control-Allow-Origin", + "login_page": "Přihlašovací stránka", + "f2b_manage_external": "Spravovat Fail2Ban externě", + "f2b_manage_external_info": "Fail2ban bude udržovat seznam zakázaných adres, ale nebude aktivně nastavovat pravidla blokování. Pro blokování použijte seznam adres níže.", + "filter": "Filtr", + "force_sso": "Vypnout přihlášení mailcow a ponechat jen SSO", + "iam": "Poskytovatel identity", + "iam_attribute_field": "Pole atributu", + "iam_authorize_url": "Autorizační koncový bod", + "iam_basedn": "Doména (Base DN)", + "iam_client_id": "ID klienta", + "iam_client_secret": "Tajný kód klienta", + "iam_client_scopes": "Scopes klienta", + "iam_default_template": "Výchozí šablona", + "iam_description": "Nastavení externího poskytovatele ověření
    Schránky uživatele se vytvoří po prvním přihlášení automaticky, pokud je tedy nastaveno mapování atributů.", + "iam_extra_permission": "Aby vše fungovalo, musí mít mailcow klient v Keycloaku nastavený servisní účet a povolení view-users.", + "iam_host": "Hostitel", + "iam_host_info": "Zadejte jeden či více hostitelů, oddělte čárkou.", + "iam_import_users": "Importovat uživatele", + "iam_auth_flow": "Proces autentizace", + "needs_restart": "potřebuje restart" }, "danger": { "access_denied": "Přístup odepřen nebo jsou neplatná data ve formuláři", @@ -408,7 +465,7 @@ "is_alias": "%s je již známa jako adresa aliasu", "is_alias_or_mailbox": "%s je již známa jako adresa aliasu, mailové schránky nebo aliasu rozvedeného z aliasu domény.", "is_spam_alias": "%s je již známa jako adresa spamového aliasu", - "last_key": "Nelze smazat poslední klíč", + "last_key": "Nelze smazat poslední klíč, vypněte tedy celé TFA.", "login_failed": "Přihlášení selhalo", "mailbox_defquota_exceeds_mailbox_maxquota": "Výchozí kvóta překračuje maximální kvótu schránky\"", "mailbox_invalid": "Název mailové schránky je neplatný", @@ -486,7 +543,20 @@ "demo_mode_enabled": "Demo režim je zapnutý", "recovery_email_failed": "Nepodařilo se odeslat e-mail pro obnovení. Obraťte se prosím na svého správce.", "password_reset_invalid_user": "Mailbox nebyl nalezen nebo není nastaven žádný e-mail pro obnovu", - "password_reset_na": "Obnovení hesla není v současné době k dispozici. Obraťte se prosím na svého správce." + "password_reset_na": "Obnovení hesla není v současné době k dispozici. Obraťte se prosím na svého správce.", + "generic_server_error": "Došlo k nečekané chybě. Obraťte se na vašeho správce.", + "to_invalid": "Adresát nemůže být prázdný", + "authsource_in_use": "Poskytovatele identity nelze změnit nebo odstranit, neboť se právě používá pro jednoho či více uživatelů.", + "iam_test_connection": "Spojení selhalo", + "img_dimensions_exceeded": "Obrázek je větší než povolené rozměry", + "img_size_exceeded": "Obrázek má větší než povolenou velikost souboru", + "invalid_reset_token": "Neplatný resetovací token", + "required_data_missing": "Chybí potřebný údaj %s", + "reset_token_limit_exceeded": "Byl překročen limit na reset tokeny. Zkuste to později.", + "max_age_invalid": "Maximální životnost %s není platná", + "mode_invalid": "Mód %s není platný", + "mx_invalid": "Záznam MX %s není platný", + "version_invalid": "Verze %s není platná" }, "datatables": { "emptyTable": "Tabulka neobsahuje žádná data", @@ -548,7 +618,8 @@ "update_failed": "Nepodařilo se zkontrolovat aktualizace", "wip": "Nedokončená vývojová verze", "memory": "Paměť", - "container_disabled": "Kontejner je zastaven nebo zakázán" + "container_disabled": "Kontejner je zastaven nebo zakázán", + "cores": "jádra" }, "diagnostics": { "cname_from_a": "Hodnota odvozena z A/AAAA záznamu. Lze použít, pokud záznam ukazuje na správný zdroj.", @@ -569,7 +640,7 @@ "alias": "Upravit alias", "allow_from_smtp": "Umožnit pouze těmto IP adresám používat SMTP", "allow_from_smtp_info": "Nechte prázdné pro povolení všech odesílatelů.
    IPv4/IPv6 adresy a sítě.", - "allowed_protocols": "Povolené protokoly", + "allowed_protocols": "Povolené protokoly pro přímá spojení (netýká se protokolů na změnu hesla)", "app_name": "Název aplikace", "app_passwd": "Heslo aplikace", "app_passwd_protocols": "Povolené protokoly pro hesla aplikací", @@ -607,7 +678,7 @@ "inactive": "Neaktivní", "kind": "Druh", "last_modified": "Naposledy změněn", - "lookup_mx": "Cíl je regulární výraz který se shoduje s MX záznamem (.*\\.google\\.com směřuje veškerou poštu na MX které jsou cílem pro google.com přes tento skok)", + "lookup_mx": "Cíl je regulární výraz, jenž se porovná s MX záznamem (např. .*\\.google\\.com na tento skok nasměruje veškerou poštu s MX, jež končí na *.google.com)", "mailbox": "Úprava mailové schránky", "mailbox_quota_def": "Výchozí kvóta schránky", "mailbox_relayhost_info": "Aplikované jen na uživatelskou schránku a přímé aliasy, přepisuje předávající server domény.", @@ -641,7 +712,7 @@ "ratelimit": "Omezení přenosu", "redirect_uri": "URL přesměrování/odvolání", "relay_all": "Předávání všech příjemců", - "relay_all_info": "Pokud se rozhodnete nepředávat všechny příjemce, musíte přidat prázdnou mailovou schránku pro každého příjemce, který se má předávat.", + "relay_all_info": "↪ Pokud se rozhodnete nepředávat všechny příjemce, musíte přidat prázdnou mailovou schránku pro každého příjemce, který se má předávat.", "relay_domain": "Předávání domény", "relay_transport_info": "
    Info
    U této domény lze pro konkrétní cíl nastavit transportní mapu. Není-li nastavena, použije se MX záznam.", "relay_unknown_only": "Předávat jen neexistující schránky. Doručení do existujících proběhne lokálně.", @@ -662,11 +733,11 @@ "sogo_visible_info": "Tato volba určuje objekty, jež lze zobrazit v SOGo (sdílené nebo nesdílené aliasy, jež ukazuje alespoň na jednu schránku).", "spam_alias": "Vytvořit nebo změnit dočasné aliasy", "spam_filter": "Spam filtr", - "spam_policy": "Přidat nebo odebrat položky whitelistu/blacklistu", + "spam_policy": "Přidat nebo odebrat položky seznamu", "spam_score": "Nastavte vlastní skóre spamu", "subfolder2": "Synchronizace do podsložky v cílovém umístění
    (prázdné = nepoužívat podsložku)", "syncjob": "Upravit synchronizační úlohu", - "target_address": "Cílová adresa/y
    (oddělte čárkou)", + "target_address": "Cílová adresa/y (oddělte čárkou)", "target_domain": "Cílová doména", "timeout1": "Časový limit pro připojení ke vzdálenému serveru", "timeout2": "Časový limit pro připojení k lokálnímu serveru", @@ -690,7 +761,26 @@ "custom_attributes": "Vlastní atributy", "footer_exclude": "Vyloučit ze zápatí", "domain_footer_skip_replies": "Ignorovat patičku u odpovědí na e-maily", - "password_recovery_email": "E-mail pro obnovu hesla" + "password_recovery_email": "E-mail pro obnovu hesla", + "mailbox_rename": "Přejmenovat schránku", + "mailbox_rename_agree": "Mám vytvořenou zálohu.", + "mailbox_rename_warning": "DŮLEŽITÉ! Vytvořte si zálohu schránky, než ji přejmenujete.", + "mailbox_rename_alias": "Automaticky vytvořit alias", + "mailbox_rename_title": "Nový název zdejší schránky", + "pushover": "Pushover", + "internal": "Interní", + "internal_info": "Interní aliasy jsou přístupné jen z vlastních domén nebo jejich aliasů.", + "mta_sts": "MTA-STS", + "mta_sts_info": "MTA-STS je standard, jenž říká poštovním serverům, aby komunikovaly pomocí TLS s platnými certifikáty.
    Používá se, pokud není k dispozici DANE, např. chybí-li či není podporováno DNSSEC.
    Pozn.: Podporuje-li přijímající doména DANE a DNSSEC, bude vždy použito DANE; MTA-STS zůstane jako plán B.", + "mta_sts_version": "Verze", + "mta_sts_version_info": "Určuje verzi standardu MTA-STS – zatím je podporována jen STSv1.", + "mta_sts_mode": "Mód", + "mta_sts_mode_info": "K dispozici jsou tři módy:
    • testing – pravidlo se jen sleduje, porušení je bez následků.
    • enforce – pravidlo je důsledně dodržováno, spojení bez platného TLS jsou odmítána.
    • none – pravidlo je zveřejněno, ale neuplatňuje se.
    ", + "mta_sts_max_age": "Maximální životnost", + "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)." }, "fido2": { "confirm": "Potvrdit", @@ -711,7 +801,7 @@ "cancel": "Zrušit", "confirm_delete": "Potvdit smazání", "delete_now": "Smazat", - "delete_these_items": "Prosím potvrďte změny objektu id:", + "delete_these_items": "Prosím potvrďte změny objektu id", "hibp_check": "Ověřit heslo v databázi hacknutých hesel haveibeenpwned.com", "hibp_nok": "Nalezeno! Toto je potenciálně nebezpečné heslo!", "hibp_ok": "Nebyla nalezena žádná shoda.", @@ -720,12 +810,12 @@ "restart_container": "Restartovat kontejner", "restart_container_info": "Důležité: Šetrný restart může chvíli trvat, prosím čekejte...", "restart_now": "Restartovat nyní", - "restarting_container": "Restartuje se kontejner, může to chvilku trvat..." + "restarting_container": "Restartuje se kontejner, může to chvilku trvat" }, "header": { "administration": "Hlavní nastavení", "apps": "Aplikace", - "debug": "Systémové informace", + "debug": "Informace", "email": "E-Mail", "mailcow_system": "Systém", "mailcow_config": "Nastavení", @@ -753,7 +843,15 @@ "new_password": "Nové heslo", "new_password_confirm": "Ověření nového hesla", "reset_password": "Obnovit heslo", - "request_reset_password": "Požádat o změnu hesla" + "request_reset_password": "Požádat o změnu hesla", + "login_domainadmintext": "Přihlášení správce domény", + "login_linkstext": "Hledáte jinou přihlašovací stránku?", + "login_usertext": "Přihlášení uživatele", + "login_admintext": "Přihlášení správce", + "login_user": "Přihlášení uživatele", + "login_dadmin": "Přihlášení správce domény", + "login_admin": "Přihlášení správce", + "email": "Mailová adresa" }, "mailbox": { "action": "Akce", @@ -785,7 +883,7 @@ "bcc": "BCC", "bcc_destination": "Cíl kopie", "bcc_destinations": "Cíl kopií", - "bcc_info": "Skryté kopie (Mapa BCC) se používá pro tiché předávání kopií všech zpráv na jinou adresu. Při použití skryté kopie typu Přijatý e-mail budou přeposlány všechny maily směřující na dotyčnou adresu nebo doménu.\nU typu Odeslaný e-mail budou přeposlány všechny maily odeslané z dotyčné adresy nebo domény.\nPokud selže přeposlání na cílovou adresu, tak odesílatel o tom nebude informován.", + "bcc_info": "Skrytá kopie (mapa BCC) se používá pro tiché předávání kopií všech zpráv na jinou adresu. Mapa příjemců se použije, funguje-li je místní cíl jako adresát zprávy. Totéž platí pro mapy odesílatelů.
    \n Místní cíl se nedozví, selže-li doručení na cíl BCC.", "bcc_local_dest": "Týká se", "bcc_map": "Skrytá kopie", "bcc_map_type": "Typ skryté kopie", @@ -840,7 +938,7 @@ "last_run_reset": "Znovu naplánovat", "mailbox": "Mailová schránka", "mailbox_defaults": "Výchozí nastavení", - "mailbox_defaults_info": "Definuje výchozí nastavení pro nové schránky", + "mailbox_defaults_info": "Definuje výchozí nastavení pro nové schránky.", "mailbox_defquota": "Výchozí velikost schránky", "mailbox_templates": "Šablony schránek", "mailbox_quota": "Max. velikost schránky", @@ -860,7 +958,7 @@ "private_comment": "Soukromý komentář", "public_comment": "Veřejný komentář", "q_add_header": "Složka nevyžádaná pošta", - "q_all": "Všechny kategorie", + "q_all": " Nevyžádaná pošta a Odmítnuta", "q_reject": "Odmítnuta", "quarantine_category": "Kategorie oznámení karantény", "quarantine_notification": "Upozornění z karantény", @@ -869,7 +967,7 @@ "recipient_map": "Mapa příjemce", "recipient_map_info": "Mapy příjemců slouží k nahrazení cílové adresy zprávy před doručením.", "recipient_map_new": "Nový přijemce", - "recipient_map_new_info": "Cílová adresa mapy příjemce musí být emailová adresa nebo název domény.", + "recipient_map_new_info": "Cílovou adresou mapy příjemců musí být emailová adresa nebo název domény.", "recipient_map_old": "Původní příjemce", "recipient_map_old_info": "Původní příjemce musí být platná emailová adresa nebo název domény.", "recipient_maps": "Mapy příjemců", @@ -888,7 +986,7 @@ "sieve_preset_5": "Automatický odpovídač (dovolená)", "sieve_preset_6": "Odmítnout zprávu s odpovědí", "sieve_preset_7": "Přesměrovat a ponechat/zahodit", - "sieve_preset_8": "Zahodit zprávu poslanou na alias, do něhož patří i odesílatel", + "sieve_preset_8": "Zprávu od určitého odesílatele přesměrovat, označit jako přečtenou a uložit do složky", "sieve_preset_header": "Vizte následující ukázková pravidla. Více informací na Wikipedii.", "sogo_visible": "Alias dostupný v SOGo", "sogo_visible_n": "Skrýt alias v SOGo", @@ -928,7 +1026,9 @@ "waiting": "Čekání", "weekly": "Každý týden", "yes": "✓", - "relay_unknown": "Předávání neexistujících schránek" + "relay_unknown": "Předávání neexistujících schránek", + "iam": "Poskytovatel identity", + "internal": "Interní" }, "oauth2": { "access_denied": "K udělení přístupu se přihlašte jako vlastník mailové schránky.", @@ -936,7 +1036,7 @@ "deny": "Zamítnout", "permit": "Ověřit aplikaci", "profile": "Profil", - "profile_desc": "Zobrazit osobní údaje: uživ. jméno, jméno, datum vytvoření a úpravy, stav", + "profile_desc": "Zobrazit osobní údaje: uživ. jméno, celé jméno, datum vytvoření a úpravy, stav", "scope_ask_permission": "Aplikace požádala o následující oprávnění" }, "quarantine": { @@ -960,7 +1060,7 @@ "notified": "Oznámeno", "qhandler_success": "Požadavek úspěšně přijat. Můžete nyní zavřít okno.", "qid": "Rspamd QID", - "qinfo": "Karanténní systém uloží odmítnutou poštu do databáze (odesílatel se nedozví, že pošta byla doručena) jakož i pošta, která bude jako kopie doručena do složky Nevyžádaná pošta. \r\n
    \"Naučit jako spam a smazat\" naučí zprávu jako spam přes Bayesian theorem a současně vypočítá fuzzy hashes pro odmítnutí podobných zpráv v budoucnosti. \r\n
    Prosím, berte na vědomí, že naučení více zpráv může být - záleží na vašem systému - časově náročné .
    Položky na černé listině jsou z karantény vyloučeny.", + "qinfo": "Karanténa uloží do databáze odmítnutou poštu (odesílatel se nedozví, že pošta byla doručena) jakož i poštu, jež se jako kopie doručuje do složky Nevyžádaná pošta.\n
    \"Naučit jako spam a smazat\" předá zprávu systému k naučení bayesiánskou analýzou jako spam a současně stanoví fuzzy hashe pro odmítání podobných zpráv v budoucnosti.\n
    Vezměte na vědomí, že učení více zpráv může být podle výkonnosti systému zabrat více času.
    Položky na seznamu zákazů jsou z karantény vyloučeny.", "qitem": "Položka v karanténě", "quarantine": "Karanténa", "quick_actions": "Akce", @@ -1005,7 +1105,8 @@ "hold_mail_legend": "Podrží vybrané e-maily. (Zabrání dalším pokusům o doručení)", "show_message": "Zobrazit zprávu", "unhold_mail": "Uvolnit", - "unhold_mail_legend": "Uvolnit vybrané e-maily k doručení. (Pouze v případě předchozího podržení)" + "unhold_mail_legend": "Uvolnit vybrané e-maily k doručení. (Pouze v případě předchozího podržení)", + "unban": "odblokovat" }, "ratelimit": { "disabled": "Vypnuto", @@ -1066,7 +1167,7 @@ "logged_in_as": "Přihlášen jako %s", "mailbox_added": "Mailová schránka %s přidána", "mailbox_modified": "Změny mailové schránky %s uloženy", - "mailbox_removed": "Mailová schránka %s odebrána", + "mailbox_removed": "Mailová schránka %s odstraněna", "nginx_reloaded": "Nginx reload byl úspěšný", "object_modified": "Změny objektu %s uloženy", "password_policy_saved": "Politika hesel byla úspěšně uložena", @@ -1100,7 +1201,14 @@ "verified_yotp_login": "Yubico OTP přihlášení ověřeno", "cors_headers_edited": "Nastavení CORS byla uložena", "domain_footer_modified": "Změny patičky domény %s byly uloženy", - "recovery_email_sent": "E-mail k obnovení byl odeslán na adresu %s" + "recovery_email_sent": "E-mail k obnovení byl odeslán na adresu %s", + "custom_login_modified": "Úpravy přihlašování úspěšně uloženy", + "domain_add_dkim_available": "Klíč DKIM už existoval", + "f2b_banlist_refreshed": "Seznam zákazů úspěšně obnoven.", + "iam_test_connection": "Spojení úspěšně navázano", + "ip_check_opt_in_modified": "Kontrola IP adresy úspěšně uložena", + "mailbox_renamed": "Schránka přejmenována z %s na %s", + "password_changed_success": "Heslo úspěšně změněno" }, "tfa": { "api_register": "%s používá Yubico Cloud API. Prosím získejte API klíč pro své Yubico ZDE", @@ -1116,7 +1224,7 @@ "none": "Deaktivovat", "reload_retry": "- (znovu načtěte stránku, opakuje-li se chyba)", "scan_qr_code": "Prosím načtěte následující kód svou aplikací na ověření nebo zadejte kód ručně.", - "select": "Prosím vyberte...", + "select": "Vyberte prosím", "set_tfa": "Nastavení způsobu dvoufaktorového ověření", "start_webauthn_validation": "Zahájit inicializaci", "tfa": "Dvoufaktorové ověření (TFA)", @@ -1125,7 +1233,10 @@ "webauthn": "WebAuthn ověření", "waiting_usb_auth": "Čeká se na USB zařízení...

    Prosím stiskněte tlačítko na svém WebAuthn USB zařízení.", "waiting_usb_register": "Čeká se na USB zařízení...

    Prosím zadejte své heslo výše a potvrďte WebAuthn registraci stiskem tlačítka na svém WebAuthn USB zařízení.", - "yubi_otp": "Yubico OTP ověření" + "yubi_otp": "Yubico OTP ověření", + "u2f_deprecated": "Zdá se, že váš klíč byl registrován zastaralou metodou U2F. Dojde k deaktivaci dvoufaktorové autentifikace a smazání klíče.", + "authenticators": "Autentifikátory", + "u2f_deprecated_important": "Registrujte svůj klíč novou metodou WebAuthn ve správě správců." }, "user": { "action": "Akce", @@ -1141,7 +1252,7 @@ "alias_time_left": "Zbývající čas", "alias_valid_until": "Platný do", "aliases_also_send_as": "Smí odesílat také jako uživatel", - "aliases_send_as_all": "Nekontrolovat přístup odesílatele pro následující doménu(y) a jejich aliasy domény:", + "aliases_send_as_all": "Nekontrolovat přístup odesílatele pro následující doménu(y) a jejich aliasy", "allowed_protocols": "Povolené protokoly", "app_hint": "Hesla aplikací jsou alternativní heslo pro přihlášení k IMAP, SMTP, CalDAV, CardDAV a EAS. Uživatelské jméno zůstává stejné.
    SOGo však nelze s heslem aplikace použít.", "app_name": "Název aplikace", @@ -1162,8 +1273,8 @@ "description": "Popis", "delete_ays": "Potvrďte odstranění.", "direct_aliases": "Přímé aliasy", - "direct_aliases_desc": "Na přímé aliasy se uplatňuje filtr spamu a nastavení pravidel TLS", - "direct_protocol_access": "Tento uživatel mailové schránky má přímý externí přístup k následujícím protokolům a aplikacím. Toto nastavení je řízeno správcem. Pro udělení přístupu k jednotlivým protokolům a aplikacím lze vytvořit hesla aplikací.
    Tlačítko \"Webmailu\" zajišťuje jednotné přihlášení k SOGo a je vždy k dispozici.", + "direct_aliases_desc": "Na přímé aliasy se uplatňuje filtr spamu a nastavení pravidel TLS.", + "direct_protocol_access": "Tento uživatel mailové schránky má přímý externí přístup k následujícím protokolům a aplikacím. Toto nastavení je řízeno správcem. Pro udělení přístupu k jednotlivým protokolům a aplikacím lze vytvořit hesla aplikací.
    Tlačítko \"Webmail\" zajišťuje jednotné přihlášení k SOGo a je vždy k dispozici.", "eas_reset": "Smazat mezipaměť zařízení ActiveSync", "eas_reset_help": "Obnovení mezipaměti zařízení pomůže zpravidla obnovit poškozený profil služby ActiveSync.
    Upozornění: Všechna data budou opětovně stažena!", "eas_reset_now": "Smazat", @@ -1204,7 +1315,7 @@ "no_last_login": "Žádný záznam o přihlášení", "no_record": "Žádný záznam", "open_logs": "Otevřít záznam", - "open_webmail_sso": "Webmailu", + "open_webmail_sso": "Webmail", "password": "Heslo", "password_now": "Současné heslo (pro potvrzení změny)", "password_repeat": "Heslo (znovu)", @@ -1236,13 +1347,13 @@ "sogo_profile_reset": "Resetovat profil SOGo", "sogo_profile_reset_help": "Tato volba odstraní uživatelský profil SOGo a nenávratně vymaže všechna data.", "sogo_profile_reset_now": "Resetovat profil", - "spam_aliases": "Dočasné e-mailové aliasy", + "spam_aliases": "Spam aliasy", "spam_score_reset": "Obnovit výchozí nastavení serveru", "spamfilter": "Filtr spamu", "spamfilter_behavior": "Hodnocení", - "spamfilter_bl": "Seznam zakázaných adres (blacklist)", - "spamfilter_bl_desc": "Zakázané emailové adresy budou vždy klasifikovány jako spam a odmítnuty. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou mailovou schránkou), s výjimkou doménových košů a samotné mailové schránky.", - "spamfilter_default_score": "Výchozí hodnoty:", + "spamfilter_bl": "Seznam zákazů", + "spamfilter_bl_desc": "Zakázané emailové adresy budou vždy klasifikovány jako spam a odmítnuty. Odmítnutá pošta se neukládá do karantény. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou poštovní schránkou), s výjimkou doménových košů a samotné poštovní schránky.", + "spamfilter_default_score": "Výchozí hodnoty", "spamfilter_green": "Zelená: tato zpráva není spam", "spamfilter_hint": "První hodnota představuje \"nízké spam skóre\" a druhá \"vysoké spam skóre\".", "spamfilter_red": "Červená: Tato zpráva je spam a server ji odmítne", @@ -1252,7 +1363,7 @@ "spamfilter_table_empty": "Žádná data k zobrazení", "spamfilter_table_remove": "smazat", "spamfilter_table_rule": "Pravidlo", - "spamfilter_wl": "Seznam povolených adres (whitelist)", + "spamfilter_wl": "Seznam povolení", "spamfilter_wl_desc": "Povolené emailové adresy nebudou nikdy klasifikovány jako spam. Lze použít zástupné znaky (*). Filtr se použije pouze na přímé aliasy (s jednou cílovou mailovou schránkou), s výjimkou doménových košů a samotné mailové schránky.", "spamfilter_yellow": "Žlutá: tato zpráva může být spam, bude označena jako spam a přesunuta do složky nevyžádané pošty", "status": "Stav", @@ -1274,7 +1385,7 @@ "tag_in_subject": "V předmětu", "text": "Text", "title": "Předmět", - "tls_enforce_in": "Vynutit TLS pro příchozí poštu ", + "tls_enforce_in": "Vynutit TLS pro příchozí poštu", "tls_enforce_out": "Vynutit TLS pro odchozí poštu", "tls_policy": "Politika šifrování", "tls_policy_warning": "Varování: Pokud se rozhodnete vynutit šifrovaný přenos pošty, může dojít ke ztrátě e-mailů.
    Zprávy, které nesplňují tuto politiku, budou mailovým systémem odmítnuty.
    Tato volba ovlivňuje primární e-mailovou adresu (přihlašovací jméno), všechny adresy odvozené z doménových aliasů i aliasy, jež mají tuto mailovou schránku jako cíl.", @@ -1290,7 +1401,16 @@ "years": "let", "pushover_sound": "Zvukové upozornění", "password_reset_info": "Pokud není zadán e-mail pro obnovení hesla, nelze tuto funkci použít.", - "pw_recovery_email": "E-mail pro obnovení hesla" + "pw_recovery_email": "E-mail pro obnovení hesla", + "tfa_info": "Dvoufaktorová autentizace vám pomáhá chránit svůj účet. Je-li zapnuta, musíte si vytvořit aplikační hesla pro aplikace, jež dvoufaktorovou autentizaci nepodporují (např. poštovní klienti).", + "attribute": "Atribut", + "authentication": "Autentifikace", + "overview": "Přehled", + "protocols": "Protokoly", + "value": "Hodnota", + "expire_never": "Nikdy nevyprší", + "forever": "Navždy", + "spam_aliases_info": "Spam alias je dočasná adresa, již lze použít k ochraně skutečných adres.
    Případně lze nastavit také dobu platnosti, po níž je alias automaticky deaktivován, čímž se řeší případy zneužitých či odcizených adres." }, "warning": { "cannot_delete_self": "Nelze smazat právě přihlášeného uživatele", diff --git a/mailcow/data/web/lang/lang.de-de.json b/mailcow/data/web/lang/lang.de-de.json index 747f7cf..762c055 100644 --- a/mailcow/data/web/lang/lang.de-de.json +++ b/mailcow/data/web/lang/lang.de-de.json @@ -25,7 +25,7 @@ "sogo_access": "Verwalten des SOGo-Zugriffsrechts erlauben", "sogo_profile_reset": "SOGo-Profil zurücksetzen", "spam_alias": "Temporäre E-Mail-Aliasse", - "spam_policy": "Blacklist/Whitelist", + "spam_policy": "Deny/Allowlist", "spam_score": "Spam-Bewertung", "syncjobs": "Sync Jobs", "tls_policy": "Verschlüsselungsrichtlinie", @@ -71,6 +71,8 @@ "goto_spam": "Nachrichten als Spam lernen", "hostname": "Host", "inactive": "Inaktiv", + "internal": "Intern", + "internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.", "kind": "Art", "mailbox_quota_def": "Standard-Quota einer Mailbox", "mailbox_quota_m": "Max. Speicherplatz pro Mailbox (MiB)", @@ -134,6 +136,7 @@ "admin_domains": "Domain-Zuweisungen", "admins": "Administratoren", "admins_ldap": "LDAP-Administratoren", + "admin_quicklink": "Quicklink zur Admin-Loginseite ausblenden", "advanced_settings": "Erweiterte Einstellungen", "api_allow_from": "IP-Adressen oder Netzwerke (CIDR Notation) für Zugriff auf API", "api_info": "Die API befindet sich noch in Entwicklung, die Dokumentation kann unter /api abgerufen werden.", @@ -146,7 +149,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 Blacklist-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 sind Indikatoren für aktive Allowlist-Einträge.", "change_logo": "Logo ändern", "configuration": "Konfiguration", "convert_html_to_text": "Konvertiere HTML zu reinem Text", @@ -155,6 +158,7 @@ "credentials_transport_warning": "Warnung: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.", "customer_id": "Kunde", "customize": "UI-Anpassung", + "login_page": "Login-Seite", "destination": "Ziel", "dkim_add_key": "ARC/DKIM-Key hinzufügen", "dkim_domains_selector": "Selector", @@ -173,6 +177,7 @@ "domain": "Domain", "domain_admin": "Administrator hinzufügen", "domain_admins": "Domain-Administratoren", + "domainadmin_quicklink": "Quicklink zur Domainadmin-Loginseite ausblenden", "domain_s": "Domain(s)", "duplicate": "Duplizieren", "duplicate_dkim": "DKIM duplizieren", @@ -181,9 +186,9 @@ "excludes": "Diese Empfänger ausschließen", "f2b_ban_time": "Bannzeit in Sekunden", "f2b_ban_time_increment": "Bannzeit erhöht sich mit jedem Bann", - "f2b_blacklist": "Blacklist für Netzwerke und Hosts", + "f2b_blacklist": "Denyliste für Netzwerke und Hosts", "f2b_filter": "Regex-Filter", - "f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. Die Aktualisierung der Liste dauert einige Sekunden.", + "f2b_list_info": "Ein Host oder Netzwerk auf der Denyliste wird immer eine Allowlist-Einheit überwiegen. Die Aktualisierung der Liste dauert einige Sekunden.", "f2b_manage_external": "Fail2Ban extern verwalten", "f2b_manage_external_info": "Fail2ban wird die Banlist weiterhin pflegen, jedoch werden keine aktiven Regeln zum blockieren gesetzt. Die unten generierte Banlist, kann verwendet werden, um den Datenverkehr extern zu blockieren.", "f2b_max_attempts": "Max. Versuche", @@ -193,8 +198,10 @@ "f2b_parameters": "Fail2ban-Parameter", "f2b_regex_info": "Berücksichtigte Logs: SOGo, Postfix, Dovecot, PHP-FPM.", "f2b_retry_window": "Wiederholungen im Zeitraum von (s)", - "f2b_whitelist": "Whitelist für Netzwerke und Hosts", + "f2b_whitelist": "Allowliste für Netzwerke und Hosts", "filter_table": "Tabelle filtern", + "force_sso_text": "Wenn ein externer OIDC-Provider konfiguriert ist, blendet diese Option die mailcow-Loginform aus und zeigt nur den Single-Sign-On-Button an.", + "force_sso": "mailcow-Login deaktivieren und nur Single Sign-On anzeigen", "forwarding_hosts": "Weiterleitungs-Hosts", "forwarding_hosts_add_hint": "Sie können entweder IPv4-/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.", "forwarding_hosts_hint": "Eingehende Nachrichten werden von den hier gelisteten Hosts bedingungslos akzeptiert. Diese Hosts werden dann nicht mit DNSBLs abgeglichen oder Greylisting unterworfen. Von ihnen empfangener Spam wird nie abgelehnt, optional kann er aber in den Spam-Ordner einsortiert werden. Die übliche Verwendung für diese Funktion ist, um Mailserver anzugeben, auf denen eine Weiterleitung zu Ihrem mailcow-Server eingerichtet wurde.", @@ -222,6 +229,7 @@ "iam_host": "Host", "iam_host_info": "Gib einen oder mehrere LDAP-Hosts ein, getrennt durch Kommas.", "iam_import_users": "Importiere Benutzer", + "iam_login_provisioning": "Benutzer beim Login erstellen", "iam_mapping": "Attribut Mapping", "iam_bindpass": "Bind Passwort", "iam_periodic_full_sync": "Vollsynchronisation", @@ -266,6 +274,7 @@ "message": "Nachricht", "message_size": "Nachrichtengröße", "nexthop": "Next Hop", + "needs_restart": "benötigt Neustart", "no": "✕", "no_active_bans": "Keine aktiven Banns", "no_new_rows": "Keine weiteren Zeilen vorhanden", @@ -308,6 +317,7 @@ "quarantine_release_format_att": "Als Anhang", "quarantine_release_format_raw": "Unverändertes Original", "quarantine_retention_size": "Rückhaltungen pro Mailbox:
    0 bedeutet inaktiv.", + "quicklink_text": "Quicklinks zu anderen Login-Seiten unter der Loginform ein- oder ausblenden", "quota_notification_html": "Benachrichtigungs-E-Mail Inhalt:
    Leer lassen, um Standard-Template wiederherzustellen.", "quota_notification_sender": "Benachrichtigungs-E-Mail Absender", "quota_notification_subject": "Benachrichtigungs-E-Mail Betreff", @@ -347,8 +357,8 @@ "rspamd_com_settings": "Ein Name wird automatisch generiert. Beispielinhalte zur Einsicht stehen nachstehend bereit. Siehe auch Rspamd docs", "rspamd_global_filters": "Globale Filter-Maps", "rspamd_global_filters_agree": "Ich werde vorsichtig sein!", - "rspamd_global_filters_info": "Globale Filter-Maps steuern globales White- und Blacklisting dieses Servers.", - "rspamd_global_filters_regex": "Die akzeptierte Form für Einträge sind ausschließlich Regular Expressions.\r\n Trotz rudimentärer Überprüfung der Map, kann es zu fehlerhaften Einträgen kommen, die Rspamd im schlechtesten Fall mit unvorhersehbarer Funktionalität bestraft.
    \r\n Das korrekte Format lautet \"/pattern/options\" (Beispiel: /.+@domain\\.tld/i).
    \r\n Der Name der Map beschreibt die jeweilige Funktion.
    \r\n Rspamd versucht die Maps umgehend aufzulösen. Bei Problemen sollte Rspamd manuell neugestartet werden.
    Elemente auf Blacklists sind von der Quarantäne ausgeschlossen.", + "rspamd_global_filters_info": "Globale Filter-Maps steuern globales Allow- und Denylisting dieses Servers.", + "rspamd_global_filters_regex": "Die akzeptierte Form für Einträge sind ausschließlich Regular Expressions.\r\n Trotz rudimentärer Überprüfung der Map, kann es zu fehlerhaften Einträgen kommen, die Rspamd im schlechtesten Fall mit unvorhersehbarer Funktionalität bestraft.
    \r\n Das korrekte Format lautet \"/pattern/options\" (Beispiel: /.+@domain\\.tld/i).
    \r\n Der Name der Map beschreibt die jeweilige Funktion.
    \r\n Rspamd versucht die Maps umgehend aufzulösen. Bei Problemen sollte Rspamd manuell neugestartet werden.
    Elemente auf Denylisten sind von der Quarantäne ausgeschlossen.", "rspamd_settings_map": "Rspamd-Settings-Map", "sal_level": "Moo-Level", "save": "Änderungen speichern", @@ -387,6 +397,7 @@ "unchanged_if_empty": "Unverändert, wenn leer", "upload": "Hochladen", "username": "Benutzername", + "user_quicklink": "Quicklink zur Benutzer-Loginseite ausblenden", "validate_license_now": "GUID erneut verifizieren", "verify": "Verifizieren", "yes": "✓", @@ -398,7 +409,9 @@ "allowed_methods": "Access-Control-Allow-Methods", "allowed_origins": "Access-Control-Allow-Origin", "logo_dark_label": "Invertiert für den Darkmode", - "logo_normal_label": "Normal" + "logo_normal_label": "Normal", + "user_link": "Nutzer-Link", + "filter": "Filter" }, "danger": { "access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten", @@ -472,10 +485,13 @@ "mailboxes_in_use": "Maximale Anzahl an Mailboxen muss größer oder gleich %d sein", "malformed_username": "Benutzername hat ein falsches Format", "map_content_empty": "Inhalt darf nicht leer sein", + "max_age_invalid": "Maximales Alter %s ist ungültig", "max_alias_exceeded": "Anzahl an Alias-Adressen überschritten", "max_mailbox_exceeded": "Anzahl an Mailboxen überschritten (%d von %d)", "max_quota_in_use": "Mailbox-Speicherplatzlimit muss größer oder gleich %d MiB sein", "maxquota_empty": "Max. Speicherplatz pro Mailbox darf nicht 0 sein.", + "mode_invalid": "Modus %s ist ungültig", + "mx_invalid": "MX-Eintrag %s ist ungültig", "mysql_error": "MySQL-Fehler: %s", "network_host_invalid": "Netzwerk oder Host ungültig: %s", "next_hop_interferes": "%s verhindert das Hinzufügen von Next Hop %s", @@ -535,10 +551,12 @@ "username_invalid": "Benutzername %s kann nicht verwendet werden", "validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an", "value_missing": "Bitte alle Felder ausfüllen", + "version_invalid": "Version %s ist ungültig", "yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s", "template_exists": "Vorlage %s existiert bereits", "template_id_invalid": "Vorlagen-ID %s ungültig", - "template_name_invalid": "Name der Vorlage ungültig" + "template_name_invalid": "Name der Vorlage ungültig", + "required_data_missing": "Die benötigten Daten: %s fehlen" }, "datatables": { "collapse_all": "Alle Einklappen", @@ -674,6 +692,8 @@ "grant_types": "Grant-types", "hostname": "Servername", "inactive": "Inaktiv", + "internal": "Intern", + "internal_info": "Interne Aliasse sind nur von der eigenen Domäne oder Alias-Domänen erreichbar.", "kind": "Art", "last_modified": "Zuletzt geändert", "lookup_mx": "Ziel mit MX vergleichen (Regex, etwa .*\\.google\\.com, um alle Ziele mit MX *google.com zu routen)", @@ -692,6 +712,17 @@ "maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)", "mbox_rl_info": "Dieses Limit wird auf den SASL Loginnamen angewendet und betrifft daher alle Absenderadressen, die der eingeloggte Benutzer verwendet. Bei Mailbox Ratelimit überwiegt ein Domain-weites Ratelimit.", "mins_interval": "Intervall (min)", + "mta_sts": "MTA-STS", + "mta_sts_info": "MTA-STS ist ein Standard, der den E-Mail-Versand zwischen Mailservern zwingt, TLS mit gültigen Zertifikaten zu verwenden.
    Er wird verwendet, wenn DANE aufgrund fehlender oder nicht unterstützter DNSSEC nicht möglich ist.
    Hinweis: Wenn die empfangende Domain DANE mit DNSSEC unterstützt, wird DANE immer bevorzugt – MTA-STS fungiert nur als Fallback.", + "mta_sts_version": "Version", + "mta_sts_version_info": "Definiert die Version des MTA-STS-Standards – derzeit ist nur STSv1 gültig.", + "mta_sts_mode": "Modus", + "mta_sts_mode_info": "Es gibt drei Modi zur Auswahl:
    • testing – Die Richtlinie wird nur überwacht, Verstöße haben keine Auswirkungen.
    • enforce – Die Richtlinie wird strikt durchgesetzt, Verbindungen ohne gültiges TLS werden abgelehnt.
    • none – Die Richtlinie wird veröffentlicht, aber nicht angewendet.
    ", + "mta_sts_max_age": "Maximales Alter", + "mta_sts_max_age_info": "Zeit in Sekunden, die empfangende Mailserver diese Richtlinie zwischenspeichern dürfen, bevor sie erneut abgerufen wird.", + "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).", "multiple_bookings": "Mehrfaches Buchen", "nexthop": "Next Hop", "none_inherit": "Keine Auswahl / Erben", @@ -738,7 +769,7 @@ "sogo_visible_info": "Diese Option hat lediglich Einfluss auf Objekte, die in SOGo darstellbar sind (geteilte oder nicht-geteilte Alias-Adressen mit dem Ziel mindestens einer lokalen Mailbox).", "spam_alias": "Anpassen temporärer Alias-Adressen", "spam_filter": "Spamfilter", - "spam_policy": "Hinzufügen und Entfernen von Einträgen in White- und Blacklists", + "spam_policy": "Hinzufügen und Entfernen von Einträgen in Allow- und Denylisten", "spam_score": "Einen benutzerdefiniterten Spam-Score festlegen", "subfolder2": "Ziel-Ordner
    (leer = kein Unterordner)", "syncjob": "Sync-Job bearbeiten", @@ -806,9 +837,13 @@ "forgot_password": "> Passwort vergessen?", "invalid_pass_reset_token": "Der Rücksetz-Token für das Passwort ist ungültig oder abgelaufen.
    Bitte fordern Sie einen neuen Link zur Passwortwiederherstellung an.", "login": "Anmelden", - "login_user": "Benutzer Anmelden", - "login_dadmin": "Domain-Administrator Anmelden", - "login_admin": "Administrator Anmelden", + "login_linkstext": "Nicht der richtige Login?", + "login_usertext": "Als Benutzer anmelden", + "login_domainadmintext": "Als Domainadmin anmelden", + "login_admintext": "Als Admin anmelden", + "login_user": "Anmeldung als Benutzer", + "login_dadmin": "Anmeldung als Domain-Administrator", + "login_admin": "Anmeldung als Administrator", "mobileconfig_info": "Bitte als Mailbox-Benutzer einloggen, um das Verbindungsprofil herunterzuladen.", "new_password": "Neues Passwort", "new_password_confirm": "Neues Passwort bestätigen", @@ -816,7 +851,8 @@ "password": "Passwort", "reset_password": "Passwort zurücksetzen", "request_reset_password": "Passwortänderung anfordern", - "username": "Benutzername" + "username": "Benutzername", + "email": "E-Mail-Adresse" }, "mailbox": { "action": "Aktion", @@ -837,7 +873,7 @@ "add_tls_policy_map": "TLS-Richtlinieneintrag hinzufügen", "address_rewriting": "Adressumschreibung", "alias": "Alias", - "alias_domain_alias_hint": "Alias-Adressen werden nicht automatisch auch auf Domain-Alias Adressen angewendet. Eine Alias-Adresse mein-alias@domain bildet demnach nicht die Adresse mein-alias@alias-domain ab.
    E-Mail-Weiterleitungen an externe Postfächer sollten über Sieve (SOGo Weiterleitung oder im Reiter \"Filter\") angelegt werden. Der Button \"Alias über Alias-Domains expandieren\" erstellt fehlende Alias-Adressen in Alias-Domains.", + "alias_domain_alias_hint": "Alias-Adressen werden nicht automatisch auch auf Domain-Alias Adressen angewendet. Eine Alias-Adresse mein-alias@domain bildet demnach nicht die Adresse mein-alias@alias-domain ab.
    E-Mail-Weiterleitungen an externe Postfächer sollten über Sieve (SOGo Weiterleitung oder im Reiter Filter) angelegt werden. Der Button Alias über Alias-Domains expandieren erstellt fehlende Alias-Adressen in Alias-Domains.", "alias_domain_backupmx": "Alias-Domain für Relay-Domain inaktiv", "aliases": "Aliasse", "allow_from_smtp": "Nur folgende IPs für SMTP erlauben", @@ -891,6 +927,7 @@ "in_use": "Prozentualer Gebrauch", "inactive": "Inaktiv", "insert_preset": "Beispiel \"%s\" laden", + "internal": "Intern", "kind": "Art", "last_mail_login": "Letzter Mail-Login", "last_modified": "Zuletzt geändert", @@ -928,7 +965,7 @@ "recipient_map_new": "Neuer Empfänger", "recipient_map_new_info": "Der neue Empfänger muss eine E-Mail-Adresse oder ein Domainname sein.", "recipient_map_old": "Original-Empfänger", - "recipient_map_old_info": "Der originale Empfänger muss eine E-Mail-Adresse oder ein Domainname sein.", + "recipient_map_old_info": "Der originäre Empfänger muss eine E-Mail-Adresse oder ein Domainname sein.", "recipient_maps": "Empfängerumschreibungen", "relay_all": "Alle Empfänger-Adressen relayen", "relay_unknown": "Unbekannte Mailboxen relayen", @@ -950,7 +987,7 @@ "sogo_visible": "Alias Sichtbarkeit in SOGo", "sogo_visible_n": "Alias in SOGo verbergen", "sogo_visible_y": "Alias in SOGo anzeigen", - "spam_aliases": "Temp. Alias", + "spam_aliases": "Spam-Alias", "stats": "Statistik", "status": "Status", "sync_jobs": "Synchronisationen", @@ -991,7 +1028,8 @@ "syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung", "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" + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen", + "iam": "Identitätsanbieter" }, "oauth2": { "access_denied": "Bitte als Mailbox-Nutzer einloggen, um den Zugriff via OAuth2 zu erlauben.", @@ -1023,7 +1061,7 @@ "notified": "Benachrichtigt", "qhandler_success": "Aktion wurde an das System übergeben. Sie dürfen dieses Fenster nun schließen.", "qid": "Rspamd QID", - "qinfo": "Das Quarantänesystem speichert abgelehnte Nachrichten in der Datenbank (dem Sender wird nicht signalisiert, dass seine E-Mail zugestellt wurde) als auch diese, die als Kopie in den Junk-Ordner der jeweiligen Mailbox zugestellt wurden.\r\n
    \"Als Spam lernen und löschen\" lernt Nachrichten nach bayesscher Statistik als Spam und erstellt Fuzzy Hashes ausgehend von der jeweiligen Nachricht, um ähnliche Inhalte zukünftig zu unterbinden.\r\n
    Der Prozess des Lernens kann abhängig vom System zeitintensiv sein.
    Auf Blacklists vorkommende Elemente sind von der Quarantäne ausgeschlossen.", + "qinfo": "Das Quarantänesystem speichert abgelehnte Nachrichten in der Datenbank (dem Sender wird nicht signalisiert, dass seine E-Mail zugestellt wurde) als auch diese, die als Kopie in den Junk-Ordner der jeweiligen Mailbox zugestellt wurden.\r\n
    \"Als Spam lernen und löschen\" lernt Nachrichten nach bayesscher Statistik als Spam und erstellt Fuzzy Hashes ausgehend von der jeweiligen Nachricht, um ähnliche Inhalte zukünftig zu unterbinden.\r\n
    Der Prozess des Lernens kann abhängig vom System zeitintensiv sein.
    Auf Denylisten vorkommende Elemente sind von der Quarantäne ausgeschlossen.", "qitem": "Quarantäneeintrag", "quarantine": "Quarantäne", "quick_actions": "Aktionen", @@ -1062,7 +1100,7 @@ "legend": "Funktionen der Mailqueue Aktionen:", "ays": "Soll die derzeitige Queue wirklich komplett bereinigt werden?", "deliver_mail": "Ausliefern", - "deliver_mail_legend": "Versucht eine erneute Zustellung der ausgwählten Mails.", + "deliver_mail_legend": "Versucht eine erneute Zustellung der ausgewählten Mails.", "hold_mail": "Zurückhalten", "hold_mail_legend": "Hält die ausgewählten Mails zurück. (Verhindert weitere Zustellversuche)", "queue_manager": "Queue Manager", @@ -1094,6 +1132,7 @@ "bcc_edited": "BCC-Map-Eintrag %s wurde geändert", "bcc_saved": "BCC- Map-Eintrag wurde gespeichert", "cors_headers_edited": "CORS Einstellungen wurden erfolgreich gespeichert", + "custom_login_modified": "Login Anpassung wurde erfolgreich gespeichert", "db_init_complete": "Datenbankinitialisierung abgeschlossen", "delete_filter": "Filter-ID %s wurde gelöscht", "delete_filters": "Filter gelöscht: %s", @@ -1231,7 +1270,7 @@ "delete_ays": "Soll der Löschvorgang wirklich ausgeführt werden?", "direct_aliases": "Direkte Alias-Adressen", "direct_aliases_desc": "Nur direkte Alias-Adressen werden für benutzerdefinierte Einstellungen berücksichtigt.", - "direct_protocol_access": "Der Hauptbenutzer hat direkten, externen Zugriff auf folgende Protokolle und Anwendungen. Diese Einstellung wird vom Administrator gesteuert. App-Passwörter können verwendet werden, um individuelle Zugänge für Protokolle und Anwendungen zu erstellen.
    Der Button \"Webmail\" kann unabhängig der Einstellung immer verwendet werden.", + "direct_protocol_access": "Der Hauptbenutzer hat direkten, externen Zugriff auf folgende Protokolle und Anwendungen. Diese Einstellung wird vom Administrator gesteuert. App-Passwörter können verwendet werden, um individuelle Zugänge für Protokolle und Anwendungen zu erstellen.
    Der Button Webmail kann unabhängig der Einstellung immer verwendet werden.", "eas_reset": "ActiveSync-Geräte-Cache zurücksetzen", "eas_reset_help": "In vielen Fällen kann ein ActiveSync-Profil durch das Zurücksetzen des Caches repariert werden.
    Vorsicht: Alle Elemente werden erneut heruntergeladen!", "eas_reset_now": "Jetzt zurücksetzen", @@ -1242,7 +1281,9 @@ "encryption": "Verschlüsselung", "excludes": "Ausschlüsse", "expire_in": "Ungültig in", + "expire_never": "Niemals ungültig", "fido2_webauthn": "FIDO2/WebAuthn", + "forever": "Für immer", "force_pw_update": "Das Passwort für diesen Benutzer muss geändert werden, damit die Zugriffssperre auf die Groupware-Komponenten wieder freigeschaltet wird.", "from": "von", "generate": "generieren", @@ -1307,12 +1348,13 @@ "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_now": "Profil jetzt zurücksetzen", - "spam_aliases": "Temporäre E-Mail-Aliasse", + "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.", "spam_score_reset": "Auf Server-Standard zurücksetzen", "spamfilter": "Spamfilter", "spamfilter_behavior": "Bewertung", - "spamfilter_bl": "Blacklist", - "spamfilter_bl_desc": "Für E-Mail-Adressen, die vom Spamfilter immer als Spam erfasst und abgelehnt werden. Die Quarantäne-Funktion ist für diese Nachrichten deaktiviert. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte nicht-\"Catch All\" Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.", + "spamfilter_bl": "Denyliste", + "spamfilter_bl_desc": "Für E-Mail-Adressen, die vom Spamfilter immer als Spam erfasst und abgelehnt werden. Die Quarantäne-Funktion ist für diese Nachrichten deaktiviert. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte Nicht-„Catch-All“-Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.", "spamfilter_default_score": "Standardwert", "spamfilter_green": "Grün: Die Nachricht ist kein Spam", "spamfilter_hint": "Der erste Wert beschreibt den \"low spam score\", der zweite Wert den \"high spam score\".", @@ -1323,8 +1365,8 @@ "spamfilter_table_empty": "Keine Einträge vorhanden", "spamfilter_table_remove": "Entfernen", "spamfilter_table_rule": "Regel", - "spamfilter_wl": "Whitelist", - "spamfilter_wl_desc": "Für E-Mail-Adressen, die vom Spamfilter nicht erfasst werden sollen. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte nicht-\"Catch All\" Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.", + "spamfilter_wl": "Allowliste", + "spamfilter_wl_desc": "Für E-Mail-Adressen, die vom Spamfilter nicht erfasst werden sollen. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte Nicht-„Catch-All“-Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.", "spamfilter_yellow": "Gelb: Die Nachricht ist vielleicht Spam, wird als Spam markiert und in den Junk-Ordner verschoben", "status": "Status", "sync_jobs": "Sync Jobs", diff --git a/mailcow/data/web/lang/lang.en-gb.json b/mailcow/data/web/lang/lang.en-gb.json index ca8bb3a..1e85259 100644 --- a/mailcow/data/web/lang/lang.en-gb.json +++ b/mailcow/data/web/lang/lang.en-gb.json @@ -25,7 +25,7 @@ "sogo_access": "Allow management of SOGo access", "sogo_profile_reset": "Reset SOGo profile", "spam_alias": "Temporary aliases", - "spam_policy": "Blacklist/Whitelist", + "spam_policy": "Denylist/Allowlist", "spam_score": "Spam score", "syncjobs": "Sync jobs", "tls_policy": "TLS policy", @@ -71,6 +71,8 @@ "goto_spam": "Learn as spam", "hostname": "Host", "inactive": "Inactive", + "internal": "Internal", + "internal_info": "Internal aliases are only accessible from the own domain or alias domains.", "kind": "Kind", "mailbox_quota_def": "Default mailbox quota", "mailbox_quota_m": "Max. quota per mailbox (MiB)", @@ -134,6 +136,7 @@ "admin_domains": "Domain assignments", "admins": "Administrators", "admins_ldap": "LDAP Administrators", + "admin_quicklink": "Hide Quicklink to Admin Login Page", "advanced_settings": "Advanced settings", "allowed_methods": "Access-Control-Allow-Methods", "allowed_origins": "Access-Control-Allow-Origin", @@ -150,7 +153,7 @@ "arrival_time": "Arrival time (server time)", "authed_user": "Auth. user", "ays": "Are you sure you want to proceed?", - "ban_list_info": "See a list of banned IPs below: network (remaining ban time) - [actions].
    IPs queued to be unbanned will be removed from the active ban list within a few seconds.
    Red labels indicate active permanent bans by blacklisting.", + "ban_list_info": "See a list of banned IPs below: network (remaining ban time) - [actions].
    IPs queued to be unbanned will be removed from the active ban list within a few seconds.
    Red labels indicate active permanent bans by denylisting.", "change_logo": "Change logo", "logo_normal_label": "Normal", "logo_dark_label": "Inverted for dark mode", @@ -161,6 +164,7 @@ "credentials_transport_warning": "Warning: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.", "customer_id": "Customer ID", "customize": "Customize", + "login_page": "Login Page", "destination": "Destination", "dkim_add_key": "Add ARC/DKIM key", "dkim_domains_selector": "Selector", @@ -179,6 +183,7 @@ "domain": "Domain", "domain_admin": "Domain administrator", "domain_admins": "Domain administrators", + "domainadmin_quicklink": "Hide Quicklink to Domainadmin Login Page", "domain_s": "Domain/s", "duplicate": "Duplicate", "duplicate_dkim": "Duplicate DKIM record", @@ -187,9 +192,9 @@ "excludes": "Excludes these recipients", "f2b_ban_time": "Ban time (s)", "f2b_ban_time_increment": "Ban time is incremented with each ban", - "f2b_blacklist": "Blacklisted networks/hosts", + "f2b_blacklist": "Denylisted networks/hosts", "f2b_filter": "Regex filters", - "f2b_list_info": "A blacklisted host or network will always outweigh a whitelist entity. List updates will take a few seconds to be applied.", + "f2b_list_info": "A denylisted host or network will always outweigh a allowlist entity. List updates will take a few seconds to be applied.", "f2b_manage_external": "Manage Fail2Ban externally", "f2b_manage_external_info": "Fail2ban will still maintain the banlist, but it will not actively set rules to block traffic. Use the generated banlist below to externally block the traffic.", "f2b_max_attempts": "Max. attempts", @@ -199,9 +204,11 @@ "f2b_parameters": "Fail2ban parameters", "f2b_regex_info": "Logs taken into consideration: SOGo, Postfix, Dovecot, PHP-FPM.", "f2b_retry_window": "Retry window (s) for max. attempts", - "f2b_whitelist": "Whitelisted networks/hosts", + "f2b_whitelist": "Allowlisted networks/hosts", "filter": "Filter", "filter_table": "Filter table", + "force_sso_text": "If an external OIDC provider is configured, this option hides the default mailcow login forms and only shows the Single Sign-On button", + "force_sso": "Disable mailcow Login and show only Single Sign-On", "forwarding_hosts": "Forwarding Hosts", "forwarding_hosts_add_hint": "You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).", "forwarding_hosts_hint": "Incoming messages are unconditionally accepted from any hosts listed here. These hosts are then not checked against DNSBLs or subjected to greylisting. Spam received from them is never rejected, but optionally it can be filed into the Junk folder. The most common use for this is to specify mail servers on which you have set up a rule that forwards incoming emails to your mailcow server.", @@ -229,6 +236,7 @@ "iam_host": "Host", "iam_host_info": "Enter one or more LDAP hosts, separated by commas.", "iam_import_users": "Import Users", + "iam_login_provisioning": "Auto-create users on login", "iam_mapping": "Attribute Mapping", "iam_bindpass": "Bind Password", "iam_periodic_full_sync": "Periodic Full Sync", @@ -273,6 +281,7 @@ "message": "Message", "message_size": "Message size", "nexthop": "Next hop", + "needs_restart": "needs restart", "no": "✕", "no_active_bans": "No active bans", "no_new_rows": "No further rows available", @@ -317,6 +326,7 @@ "quarantine_release_format_att": "As attachment", "quarantine_release_format_raw": "Unmodified original", "quarantine_retention_size": "Retentions per mailbox:
    0 indicates inactive.", + "quicklink_text": "Show or hide quick links to other login pages under the login form", "quota_notification_html": "Notification email template:
    Leave empty to restore default template.", "quota_notification_sender": "Notification email sender", "quota_notification_subject": "Notification email subject", @@ -357,8 +367,8 @@ "rspamd_com_settings": "A setting name will be auto-generated, please see the example presets below. For more details see Rspamd docs", "rspamd_global_filters": "Global filter maps", "rspamd_global_filters_agree": "I will be careful!", - "rspamd_global_filters_info": "Global filter maps contain different kind of global black and whitelists.", - "rspamd_global_filters_regex": "Their names explain their purpose. All content must contain valid regular expression in the format of \"/pattern/options\" (e.g. /.+@domain\\.tld/i).
    \r\n Although rudimentary checks are being executed on each line of regex, Rspamds functionality can be broken, if it fails to read the syntax correctly.
    \r\n Rspamd will try to read the map content when changed. If you experience problems, restart Rspamd to enforce a map reload.
    Blacklisted elements are excluded from quarantine.", + "rspamd_global_filters_info": "Global filter maps contain different kind of global deny and allowlists.", + "rspamd_global_filters_regex": "Their names explain their purpose. All content must contain valid regular expression in the format of \"/pattern/options\" (e.g. /.+@domain\\.tld/i).
    \r\n Although rudimentary checks are being executed on each line of regex, Rspamds functionality can be broken, if it fails to read the syntax correctly.
    \r\n Rspamd will try to read the map content when changed. If you experience problems, restart Rspamd to enforce a map reload.
    Denylisted elements are excluded from quarantine.", "rspamd_settings_map": "Rspamd settings map", "sal_level": "Moo level", "save": "Save changes", @@ -398,6 +408,7 @@ "upload": "Upload", "username": "Username", "user_link": "User-Link", + "user_quicklink": "Hide Quicklink to User Login Page", "validate_license_now": "Validate GUID against license server", "verify": "Verify", "yes": "✓" @@ -474,10 +485,13 @@ "mailboxes_in_use": "Max. mailboxes must be greater or equal to %d", "malformed_username": "Malformed username", "map_content_empty": "Map content cannot be empty", + "max_age_invalid": "Max age %s is invalid", "max_alias_exceeded": "Max. aliases exceeded", "max_mailbox_exceeded": "Max. mailboxes exceeded (%d of %d)", "max_quota_in_use": "Mailbox quota must be greater or equal to %d MiB", "maxquota_empty": "Max. quota per mailbox must not be 0.", + "mode_invalid": "Mode %s is invalid", + "mx_invalid": "MX record %s is invalid", "mysql_error": "MySQL error: %s", "network_host_invalid": "Invalid network or host: %s", "next_hop_interferes": "%s interferes with nexthop %s", @@ -541,6 +555,7 @@ "username_invalid": "Username %s cannot be used", "validity_missing": "Please assign a period of validity", "value_missing": "Please provide all values", + "version_invalid": "Version %s is invalid", "yotp_verification_failed": "Yubico OTP verification failed: %s" }, "datatables": { @@ -677,6 +692,8 @@ "grant_types": "Grant types", "hostname": "Hostname", "inactive": "Inactive", + "internal": "Internal", + "internal_info": "Internal aliases are only accessible from the own domain or alias domains.", "kind": "Kind", "last_modified": "Last modified", "lookup_mx": "Destination is a regular expression to match against MX name (.*\\.google\\.com to route all mail targeted to a MX ending in google.com over this hop)", @@ -695,6 +712,17 @@ "maxbytespersecond": "Max. bytes per second
    (0 = unlimited)", "mbox_rl_info": "This rate limit is applied on the SASL login name, it matches any \"from\" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.", "mins_interval": "Interval (min)", + "mta_sts": "MTA-STS", + "mta_sts_info": "MTA-STS is a standard that enforces email delivery between mail servers to use TLS with valid certificates.
    It is used when DANE is not possible due to missing or unsupported DNSSEC.
    Note: If the receiving domain supports DANE with DNSSEC, DANE is always preferred – MTA-STS only acts as a fallback.", + "mta_sts_version": "Version", + "mta_sts_version_info": "Defines the version of the MTA-STS standard – currently only STSv1 is valid.", + "mta_sts_mode": "Mode", + "mta_sts_mode_info": "There are three modes to choose from:
    • testing – policy is only monitored, violations have no impact.
    • enforce – policy is strictly enforced, connections without valid TLS are rejected.
    • none – policy is published but not applied.
    ", + "mta_sts_max_age": "Max age", + "mta_sts_max_age_info": "Time in seconds that receiving mail servers may cache this policy until refetching.", + "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).", "multiple_bookings": "Multiple bookings", "none_inherit": "None / Inherit", "nexthop": "Next hop", @@ -742,7 +770,7 @@ "sogo_visible_info": "This option only affects objects, that can be displayed in SOGo (shared or non-shared alias addresses pointing to at least one local mailbox). If hidden, an alias will not appear as selectable sender in SOGo.", "spam_alias": "Create or change time limited alias addresses", "spam_filter": "Spam filter", - "spam_policy": "Add or remove items to white-/blacklist", + "spam_policy": "Add or remove items to allow-/denylist", "spam_score": "Set a custom spam score", "subfolder2": "Sync into subfolder on destination
    (empty = do not use subfolder)", "syncjob": "Edit sync job", @@ -809,6 +837,10 @@ "forgot_password": "> Forgot Password?", "invalid_pass_reset_token": "The reset password token is invalid or has expired.
    Please request a new password reset link.", "login": "Login", + "login_linkstext": "Not the correct login?", + "login_usertext": "Log in as user", + "login_domainadmintext": "Log in as domain admin", + "login_admintext": "Log in as admin", "login_user": "User Login", "login_dadmin": "Domain-Administrator Login", "login_admin": "Administrator Login", @@ -819,7 +851,8 @@ "password": "Password", "reset_password": "Reset Password", "request_reset_password": "Request password change", - "username": "Username" + "username": "Username", + "email": "Email address" }, "mailbox": { "action": "Action", @@ -899,6 +932,7 @@ "in_use": "In use (%)", "inactive": "Inactive", "insert_preset": "Insert example preset \"%s\"", + "internal": "Internal", "kind": "Kind", "last_mail_login": "Last mail login", "last_modified": "Last modified", @@ -1027,7 +1061,7 @@ "notified": "Notified", "qhandler_success": "Request successfully sent to the system. You can now close the window.", "qid": "Rspamd QID", - "qinfo": "The quarantine system will save rejected mail to the database (the sender will not be given the impression of a delivered mail) as well as mail, that is delivered as copy into the Junk folder of a mailbox.\r\n
    \"Learn as spam and delete\" will learn a message as spam via Bayesian theorem and also calculate fuzzy hashes to deny similar messages in the future.\r\n
    Please be aware that learning multiple messages can be - depending on your system - time consuming.
    Blacklisted elements are excluded from the quarantine.", + "qinfo": "The quarantine system will save rejected mail to the database (the sender will not be given the impression of a delivered mail) as well as mail, that is delivered as copy into the Junk folder of a mailbox.\r\n
    \"Learn as spam and delete\" will learn a message as spam via Bayesian theorem and also calculate fuzzy hashes to deny similar messages in the future.\r\n
    Please be aware that learning multiple messages can be - depending on your system - time consuming.
    Denylisted elements are excluded from the quarantine.", "qitem": "Quarantine item", "quarantine": "Quarantine", "quick_actions": "Actions", @@ -1105,6 +1139,7 @@ "bcc_edited": "BCC map entry %s edited", "bcc_saved": "BCC map entry saved", "cors_headers_edited": "CORS settings have been saved", + "custom_login_modified": "Login customisation was saved successfully", "db_init_complete": "Database initialization completed", "delete_filter": "Deleted filters ID %s", "delete_filters": "Deleted filters: %s", @@ -1253,7 +1288,9 @@ "encryption": "Encryption", "excludes": "Excludes", "expire_in": "Expire in", + "expire_never": "Never Expire", "fido2_webauthn": "FIDO2/WebAuthn", + "forever": "Forever", "force_pw_update": "You must set a new password to be able to access groupware related services.", "from": "from", "generate": "generate", @@ -1320,12 +1357,13 @@ "sogo_profile_reset": "Reset SOGo profile", "sogo_profile_reset_help": "This will destroy a user's SOGo profile and delete all contact and calendar data irretrievable.", "sogo_profile_reset_now": "Reset profile now", - "spam_aliases": "Temporary email aliases", + "spam_aliases": "Spam email aliases", + "spam_aliases_info": "A spam alias is a temporary email address that can be used to protect real email addresses.
    Optionally, an expiration time can be set so that the alias is automatically deactivated after the defined period, effectively disposing of abused or leaked addresses.", "spam_score_reset": "Reset to server default", "spamfilter": "Spam filter", "spamfilter_behavior": "Rating", - "spamfilter_bl": "Blacklist", - "spamfilter_bl_desc": "Blacklisted email addresses to always classify as spam and reject. Rejected mail will not be copied to quarantine. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.", + "spamfilter_bl": "Denylist", + "spamfilter_bl_desc": "Denylisted email addresses to always classify as spam and reject. Rejected mail will not be copied to quarantine. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.", "spamfilter_default_score": "Default values", "spamfilter_green": "Green: this message is not spam", "spamfilter_hint": "The first value describes the \"low spam score\", the second represents the \"high spam score\".", @@ -1336,8 +1374,8 @@ "spamfilter_table_empty": "No data to display", "spamfilter_table_remove": "remove", "spamfilter_table_rule": "Rule", - "spamfilter_wl": "Whitelist", - "spamfilter_wl_desc": "Whitelisted email addresses are programmed to never classify as spam. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.", + "spamfilter_wl": "Allowlist", + "spamfilter_wl_desc": "Allowlisted email addresses are programmed to never classify as spam. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.", "spamfilter_yellow": "Yellow: this message may be spam, will be tagged as spam and moved to your junk folder", "status": "Status", "sync_jobs": "Sync jobs", diff --git a/mailcow/data/web/lang/lang.es-es.json b/mailcow/data/web/lang/lang.es-es.json index 814142c..7d86973 100644 --- a/mailcow/data/web/lang/lang.es-es.json +++ b/mailcow/data/web/lang/lang.es-es.json @@ -10,11 +10,11 @@ "quarantine": "Acciones de cuarentena", "quarantine_attachments": "Archivos ajuntos en cuarentena", "quarantine_notification": "Notificaciones de cuarentena", - "ratelimit": "Rate limit", + "ratelimit": "Límite de peticiones", "recipient_maps": "Rutas del destinatario", "sogo_profile_reset": "Resetear perfil SOGo", "spam_alias": "Aliases temporales", - "spam_policy": "Lista blanca/negra", + "spam_policy": "Lista de bloqueo/desbloqueo", "spam_score": "Puntuación de spam", "syncjobs": "Trabajos de sincronización", "tls_policy": "Póliza de TLS", @@ -25,8 +25,10 @@ "quarantine_category": "Cambiar categoría de las notificaciones de cuarentena", "domain_relayhost": "Cambiar relayhost por un dominio", "extend_sender_acl": "Permitir extender la ACL del remitente por direcciones externas", - "pw_reset": "Permitir el reset de la contraseña del usario mailcow", - "sogo_access": "Permitir la gestión del acceso a SOGo" + "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" }, "add": { "activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.", @@ -34,7 +36,7 @@ "add": "Agregar", "add_domain_only": "Agregar dominio solamente", "add_domain_restart": "Agregar dominio y reiniciar SOGo", - "alias_address": "Dirección(es) alias:", + "alias_address": "Dirección(es) alias", "alias_address_info": "Dirección(es) de correo completa(s) ó @dominio.com, para atrapar todos los mensajes para un dominio (separado por coma). Dominios que existan en mailcow solamente.", "alias_domain": "Dominio alias", "alias_domain_info": "Nombres de dominio válidos solamente (separado por coma).", @@ -45,13 +47,13 @@ "delete1": "Eliminar de la fuente cuando se complete", "delete2": "Eliminar mensajes en el destino que no están en la fuente", "delete2duplicates": "Eliminar duplicados en el destino", - "description": "Descripción:", + "description": "Descripción", "destination": "Destino", "domain": "Dominio", - "domain_quota_m": "Cuota total del dominio (MiB):", + "domain_quota_m": "Cuota total del dominio (MiB)", "enc_method": "Método de cifrado", "exclude": "Excluir objectos (regex)", - "full_name": "Nombre completo:", + "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.", "generate": "Generar", @@ -61,20 +63,20 @@ "hostname": "Host", "kind": "Tipo", "mailbox_quota_def": "Cuota de buzón predeterminada", - "mailbox_quota_m": "Máx. cuota por buzón (MiB):", - "mailbox_username": "Nombre de usuario (parte izquierda de una dirección de correo):", - "max_aliases": "Máx. alias posibles:", - "max_mailboxes": "Máx. buzones posibles:", + "mailbox_quota_m": "Máx. cuota por buzón (MiB)", + "mailbox_username": "Nombre de usuario (parte izquierda de una dirección de correo)", + "max_aliases": "Máx. alias posibles", + "max_mailboxes": "Máx. buzones posibles", "mins_interval": "Intervalo de sondeo (minutos)", "multiple_bookings": "Múltiples reservas", "nexthop": "Siguiente destino", - "password": "Constraseña:", - "password_repeat": "Confirmación de contraseña (repetir):", + "password": "Contraseña", + "password_repeat": "Confirmación de contraseña (repetir)", "port": "Puerto", - "post_domain_add": "Nota: Necesitarás reiniciar el contenedor del servicio SOGo despues de agregar un nuevo dominio", - "quota_mb": "Cuota (MiB):", + "post_domain_add": "Es necesario reiniciar el contenedor del servicio SOGo, \"sogo-mailcow\", tras agregar un nuevo dominio.

    Además, la configuración DNS de los dominios debería ser comprobada. En cuanto la configuración DNS se apruebe, reinicie \"acme-mailcow\" para generar automáticamente certificados para su nuevo dominio (autoconfig.<dominio>, autodiscover.<dominio>).
    Este paso es opcional y se reintentará cada 24 horas.", + "quota_mb": "Cuota (MiB)", "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 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", @@ -83,7 +85,7 @@ "skipcrossduplicates": "Omitir mensajes duplicados en carpetas (orden de llegada)", "subscribeall": "Suscribirse a todas las carpetas", "syncjob": "Añadir trabajo de sincronización", - "syncjob_hint": "Ten en cuenta que las contraseñas deben guardarse en texto sin cifrado", + "syncjob_hint": "Tenga en cuenta que las contraseñas deben guardarse en texto plano sin cifrar", "target_address": "Direcciones destino:", "target_address_info": "Dirección(es) de correo completa(s) (separado por coma).", "target_domain": "Dominio destino:", @@ -100,7 +102,13 @@ "comment_info": "Los comentarios privados no son visibles al usuario, mientras que los comentarios públicos aparecerán sobre la información general del usuario", "dry": "Simular la sincronización", "private_comment": "Comentario privado", - "app_passwd_protocols": "Protocolos autorizados para la contraseña de la aplicación" + "app_passwd_protocols": "Protocolos autorizados para la contraseña de la aplicación", + "relay_transport_info": "
    Información
    Puede definir mapas de transporte para un destino personalizado para este dominio. En caso de no definirlo, se realizará una búsqueda MX.", + "bcc_dest_format": "El destino del CCO debe ser una única dirección de correo electrónico válida.
    Si necesita enviar una copia a varias direcciones, cree un alias y utilícelo aquí.", + "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" }, "admin": { "access": "Acceso", @@ -129,7 +137,7 @@ "app_name": "Nombre de la app", "apps_name": "Nombre \"mailcow Apps\"", "arrival_time": "Tiempo de llegada (hora del servidor)", - "ban_list_info": "Lista de IPs bloqueadas: red (tiempo de prohibición restante) - [acciones].
    Las IPs en cola para ser desbloqueadas se eliminarán de la lista de bloqueos en unos pocos segundos.
    Las etiquetas rojas indican bloqueos permanentes mediante la inclusión en la lista negra.", + "ban_list_info": "Lista de direcciones IP bloqueadas: red (tiempo de prohibición restante) - [acciones].
    Las direcciones IP en cola para ser desbloqueadas se eliminarán de la lista de bloqueos en unos segundos.
    Las etiquetas rojas indican bloqueos permanentes por inclusión en la lista de bloqueo.", "change_logo": "Cambiar logo", "configuration": "Configuración", "credentials_transport_warning": "Advertencia: al agregar una nueva entrada de ruta de transporte se actualizarán las credenciales para todas las entradas con una columna de \"siguiente destino\" coincidente.", @@ -157,15 +165,15 @@ "excludes": "Excluye a estos destinatarios", "f2b_ban_time": "Tiempo de restricción (s)", "f2b_ban_time_increment": "Tiempo de restricción se incrementa con cada restricción", - "f2b_blacklist": "Redes y hosts en lista negra", - "f2b_list_info": "Un host o red en lista negra siempre superará a una entidad de la lista blanca. Las actualizaciones de la lista tardarán unos segundos en aplicarse.", + "f2b_blacklist": "Redes y hosts en lista de bloqueo", + "f2b_list_info": "Un host o red en lista de bloqueo siempre tendrá prioridad sobre una entidad de la lista de desbloqueo. Las actualizaciones de la lista tardarán unos segundos en aplicarse.", "f2b_max_attempts": "Max num. de intentos", "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_retry_window": "Ventana de tiempo entre reintentos", - "f2b_whitelist": "Redes y hosts en lista blanca", + "f2b_whitelist": "Redes y hosts en lista de desbloqueo", "filter_table": "Filtrar tabla", "forwarding_hosts": "Hosts de reenvío", "forwarding_hosts_add_hint": "Se puede especificar direcciones IPv4 / IPv6, redes en notación CIDR, nombres de host (que se resolverán en direcciones IP) o dominios (que se resolverán en direcciones IP consultando registros SPF o, en su defecto, registros MX)", @@ -253,7 +261,146 @@ "unban_pending": "Desbloqueo pendiente", "unchanged_if_empty": "Si no hay cambios déjalo en blanco", "upload": "Cargar", - "username": "Nombre de usuario" + "username": "Nombre de usuario", + "force_sso_text": "Si se configura un proveedor OIDC externo, esta opción oculta los formularios por defecto de inicio de sesión y muestra solamente el botón de inicio de sesión único", + "admin_quicklink": "Ocultar enlace rápido a página de inicio de sesión para administradores", + "iam_default_template_description": "Si no se asigna una plantilla a un usuario, se utilizará la plantilla por defecto para crear el buzón, pero no para actualizarlo.", + "reset_password_vars": "{{link}} El enlace generado para el restablecimiento de contraseña
    {{username}} El buzón del usuario que ha solicitado el restablecimiento de contraseña
    {{username2}} La dirección del buzón de recuperación de contraseña
    {{date}} La fecha en que se realizó la solicitud de restablecimiento de contraseña
    {{token_lifetime}} El periodo de vigencia del token en minutos
    {{hostname}} El servidor Mailcow", + "api_info": "La API es un trabajo en curso. La documentación se puede encontrar en /api", + "iam_description": "Configurar un proveedor de autenticación externo
    Los buzones de usuario se crearán automáticamente la primera vez que se inicie sesión, siempre que se hayan configurado las equivalencias de atributos", + "ui_header_announcement_help": "El anuncio será visible para todos los usuarios conectados y también en la pantalla de inicio de sesión.", + "html": "HTML", + "oauth2_redirect_uri": "URI de redirección", + "quarantine_bcc": "Remitir una copia de todas las notificaciones (CCO) a este destinatario:
    Dejar en blanco para desactivar. Correo sin firmar y sin comprobar. Debe entregarse solo internamente.", + "quarantine_redirect": "Redirigir todas las notificaciones a este destinatario:
    Dejar en blanco para desactivar. Correo sin firmar y sin comprobar. Debe entregarse sólo internamente.", + "iam_authorize_url": "Endpoint de autorización", + "sal_level": "Nivel de Moo", + "ui_footer": "Pie de página (se permite HTML)", + "is_mx_based": "Basado en MX", + "password_reset_tmpl_text": "Plantilla de texto", + "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.", + "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", + "iam_port": "Puerto", + "iam_realm": "Ámbito", + "iam_redirect_url": "URL de redirección", + "iam_server_url": "URL del servidor", + "iam_sso": "Inicio de sesión único (SSO)", + "iam_sync_interval": "Intervalo de sincronización/importación (minutos)", + "iam_test_connection": "Comprobar conexión", + "iam_token_url": "Endpoint del token", + "iam_username_field": "Campo de nombre de usuario", + "iam_use_ssl": "Utilizar SSL", + "iam_use_tls": "Utilizar STARTTLS", + "iam_userinfo_url": "Endopint de información de usuario", + "iam_use_tls_info": "Si se habilita TLS, se debe utilizar el puerto por defecto del servidor LDAP (389). No se permiten puertos SSL.", + "iam_version": "Versión", + "ignore_ssl_error": "Ignorar errores de SSL", + "ip_check": "Comprobación IP", + "ip_check_disabled": "La comprobación de IP está desactivada. Puede activarla en
    Sistema > Configuración > Opciones > Personalizar.", + "ip_check_opt_in": "Aceptar utilizar el servicio de terceros ipv4.mailcow.email y ipv6.mailcow.email para resolver direcciones IP externas.", + "last_applied": "Aplicado por última vez", + "license_info": "No es obligatorio contar con una licencia, pero ayuda a continuar el desarrollo.
    Indique aquí su GUID o adquiera servicios de soporte para su instalación de Mailcow.", + "login_time": "Hora de inicio de sesión", + "lookup_mx": "Destino es una expresión regular con la que contrastar el nombre MX (.*\\.google\\.com para dirigir todo el tráfico dirigido a un MX que termina en google.com a través de este salto)", + "message": "Mensaje", + "no": "✕", + "optional": "opcional", + "app_hide": "Ocultar para inicio de sesión", + "convert_html_to_text": "Convertir HTML a texto plano", + "cors_settings": "Configuración de CORS", + "customer_id": "ID de cliente", + "dkim_overwrite_key": "Sobrescribir la clave DKIM existente", + "domain_admin": "Administrador de dominio", + "f2b_manage_external": "Gestionar Fail2Ban de manera externa", + "f2b_manage_external_info": "Fail2Ban conservará la lista de bloqueo, pero no establecerá activamente reglas para bloquear el tráfico. Utilizar la lista de bloqueo siguiente para bloquear externamente el tráfico.", + "filter": "Filtrar", + "admins": "Administradores", + "admins_ldap": "Administradores de LDAP", + "advanced_settings": "Configuración avanzada", + "allowed_methods": "Access-Control-Allow-Methods", + "allowed_origins": "Access-Control-Allow-Origin", + "api_read_only": "Acceso de sólo lectura", + "api_read_write": "Acceso de lectura y escritura", + "api_skip_ip_check": "Omitir la comprobación de la IP para la API", + "authed_user": "Usuario autentificado", + "ays": "¿Está seguro de querer continuar?", + "logo_normal_label": "Normal", + "logo_dark_label": "Invertido para modo oscuro", + "copy_to_clipboard": "¡Texto copiado al portapapeles!", + "login_page": "Inicio de sesión", + "domainadmin_quicklink": "Ocultar enlace rápido a página de inicio de sesión para administradores de dominios", + "domain_s": "Dominio(s)", + "f2b_filter": "Filtros regex", + "f2b_regex_info": "Registros tomados en consideración: SOGo, Postfix, Dovecot, PHP-FPM.", + "force_sso": "Deshabilitar el inicio de sesión de Mailcow y mostrar solamente el inicio de sesión único", + "guid": "GUID - ID de instancia único", + "guid_and_license": "GUID y licencia", + "hash_remove_info": "Al eliminar un hash de límite de velocidad (si todavía existe) se reiniciará su contador por completo.
    Cada hash se indica con un color individual.", + "iam": "Proveedor de identidad", + "iam_attribute_field": "Campo de atributo", + "iam_auth_flow": "Flujo de autenticación", + "iam_basedn": "DN de base", + "iam_client_id": "ID de cliente", + "iam_client_secret": "Secreto de cliente", + "iam_client_scopes": "Ámbitos de cliente", + "iam_default_template": "Plantilla por defecto", + "iam_host": "Host", + "iam_host_info": "Introduzca uno o más hosts de LDAP, separados por comas.", + "iam_import_users": "Importar usuarios", + "iam_mapping": "Equivalencias de atributos", + "needs_restart": "necesita reinicio", + "oauth2_apps": "Aplicaciones OAuth2", + "oauth2_add_client": "Añadir cliente OAuth2", + "oauth2_renew_secret": "Generar nuevo secreto de cliente", + "oauth2_revoke_tokens": "Revocar todos los tokens de cliente", + "options": "Opciones", + "password_policy": "Política de contraseñas", + "password_policy_chars": "Debe contener al menos un caracter alfabético", + "password_policy_length": "La longitud mínima de la contraseña es %d", + "password_policy_numbers": "Debe contener al menos un número", + "password_policy_special_chars": "Debe contener caracteres especiales", + "password_reset_info": "Si no se indica una dirección para la recuperación de contraseñas, esta función no puede utilizarse.", + "password_reset_settings": "Configuración de recuperación de contraseña", + "password_reset_tmpl_html": "Plantilla HTML", + "password_settings": "Configuración de contraseña", + "priority": "Prioridad", + "quarantine_max_score": "Descartar notificación si la puntuación de spam de un mensaje de correo es mayor que este valor:
    Por defecto 9999.0", + "queue_unban": "desbloquear", + "regex_maps": "Mapas regex", + "relay_rcpt": "Dirección \"Para:\"", + "reset_limit": "Eliminar hash", + "restore_template": "Dejar en blanco para restablecer la plantilla por defecto", + "rsetting_no_selection": "Seleccione una regla", + "rsettings_preset_3": "Permitir solamente remitentes específicos para un buzón (utilizar únicamente como buzón interno)", + "rsettings_preset_4": "Deshabilitar Rspamd para un dominio", + "rspamd_global_filters": "Mapas de filtrado globales", + "rspamd_global_filters_agree": "¡Tendré cuidado!", + "rspamd_global_filters_info": "Los mapas de filtrado globales contienen distintos tipos de listos de bloqueo y desbloqueo.", + "service": "Servicio", + "service_id": "ID de servicio", + "success": "Éxito", + "task": "Tarea", + "time": "Tiempo", + "title": "Título", + "transport_dest_format": "Regex o sintaxis: ejemplo.org, .ejemplo.org, *, buzon@ejemplo.org (se pueden introducir varios valores separados por comas)", + "transport_test_rcpt_info": "• Utilizar null@hosted.mailcow.de para comprobar la retransmisión a un destino externo.", + "ui_header_announcement": "Anuncios", + "ui_header_announcement_content": "Texto (se permite HTML)", + "ui_header_announcement_select": "Seleccionar tipo de anuncio", + "ui_header_announcement_type": "Tipo", + "ui_header_announcement_type_danger": "Muy importante", + "ui_header_announcement_type_info": "Información", + "ui_header_announcement_type_warning": "Importante", + "user_link": "Enlace de usuario", + "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": "✓" }, "danger": { "access_denied": "Acceso denegado o datos del formulario inválidos", @@ -341,7 +488,60 @@ "username_invalid": "Nombre de usuario no se puede utilizar", "validity_missing": "Por favor asigna un periodo de validez", "value_missing": "Por favor proporcione todos los valores", - "yotp_verification_failed": "Verificación Yubico OTP fallida: %s" + "yotp_verification_failed": "Verificación Yubico OTP fallida: %s", + "last_key": "La última clave no se puede eliminar, en su lugar desactive la autenticación de doble factor.", + "img_dimensions_exceeded": "La imagen excede el tamaño máximo permitido", + "authsource_in_use": "El proveedor de identidades no se puede cambiar al estar en uso por uno o más usuario(s).", + "app_name_empty": "El nombre de la aplicación no puede quedar vacío", + "recovery_email_failed": "No se ha podido enviar un correo de recuperación. Contacte con su administrador.", + "tls_policy_map_dest_invalid": "Destino de política no válido", + "cors_invalid_method": "Allow-Method especificado no válido", + "dkim_domain_or_sel_exists": "Ya existe una clave DKIM para \"%s\" y no será sobrescrita", + "webauthn_publickey_failed": "No se ha almacenado ninguna clave pública para el autenticador seleccionado", + "invalid_reset_token": "Token de restablecimiento no válido", + "password_reset_na": "El restablecimiento de contraseña no está disponible en estos momentos. Contacte con su administrador.", + "generic_server_error": "Se ha producido un error inesperado en el servidor. Contacte con su administrador.", + "reset_f2b_regex": "El filtro Regex no se ha podido restablecer a tiempo, inténtelo de nuevo o espere unos segundos y vuelva a cargar la página web.", + "extra_acl_invalid": "Dirección de remitente externo \"%s\" no válida", + "extra_acl_invalid_domain": "El remitente externo \"%s\" utiliza un dominio no válido", + "max_alias_exceeded": "Se ha excedido el número máximo de alias", + "app_passwd_id_invalid": "Contraseña de aplicación con ID %s no válida", + "comment_too_long": "Comentario demasiado largo, máximo de 160 caracteres permitidos", + "cors_invalid_origin": "Allow-Origin especificado no válido", + "demo_mode_enabled": "Modo demo activado", + "description_invalid": "Descripción de recurso para %s no válida", + "extended_sender_acl_denied": "no se encuentra ACL para establecer direcciones de remitente externo", + "fido2_verification_failed": "Verificación FIDO2 fallida: %s", + "file_open_error": "El archivo no se puede abrir para escritura", + "global_filter_write_error": "No se ha podido escribir el archivo de filtro: %s", + "global_map_invalid": "Mapa global con ID %s no válido", + "global_map_write_error": "No se ha podido guardar el mapa global con ID %s: %s", + "ham_learn_error": "Error de aprendizaje de correo deseado: %s", + "iam_test_connection": "Conexión fallida", + "imagick_exception": "Error: Excepción en Imagick al leer la imagen", + "img_invalid": "No se ha podido validar el archivo de imagen", + "img_size_exceeded": "La imagen excede el tamaño máximo de archivo", + "img_tmp_missing": "No se ha podido validar el archivo de imagen: archivo temporal no encontrado", + "invalid_filter_type": "Tipo de filtro no válido", + "invalid_mime_type": "Tipo MIME no válido", + "maxquota_empty": "La cuota máxima por buzón no debe ser cero.", + "nginx_reload_failed": "Recarga de Nginx fallida: %s", + "password_reset_invalid_user": "Buzón no encontrado o dirección de correo para recuperación no establecida", + "pushover_credentials_missing": "Falta el token y/o la clave de Pushover", + "pushover_key": "La clave de Pushover tiene un formato incorrecto", + "pushover_token": "El token de Pushover tiene un formato incorrecto", + "required_data_missing": "Datos necesarios %s no proporcionados", + "reset_token_limit_exceeded": "Se ha superado el límite de tokens de restablecimiento. Inténtelo más tarde.", + "resource_invalid": "Nombre de recurso %s no válido", + "targetd_relay_domain": "El dominio de destino %s es un dominio de retransmisión", + "template_exists": "La plantilla %s ya existe", + "template_id_invalid": "Plantilla con ID %s no válida", + "template_name_invalid": "Nombre de plantilla no válido", + "temp_error": "Error transitorio", + "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" }, "debug": { "containers_info": "Información de los contenedores", @@ -357,7 +557,29 @@ "started_at": "Iniciado el", "uptime": "Uptime", "static_logs": "Logs estáticos", - "system_containers": "Sistema y Contenedores" + "system_containers": "Sistema y Contenedores", + "show_ip": "Mostrar IP pública", + "wip": "Actualmente incompleto", + "current_time": "Hora del sistema", + "service": "Servicio", + "timezone": "Huso horario", + "update_available": "Hay una actualización disponible", + "update_failed": "No se han podido comprobar las actualizaciones", + "architecture": "Arquitectura", + "chart_this_server": "Gráfico (este servidor)", + "container_running": "En ejecución", + "container_disabled": "Contenedor detenido o desactivado", + "container_stopped": "Detenido", + "cores": "Núcleos", + "error_show_ip": "No se han podido resolver las direcciones IP públicas", + "history_all_servers": "Historial (todos los servidores)", + "login_time": "Tiempo", + "memory": "Memoria", + "online_users": "Usuarios conectados", + "started_on": "Iniciado", + "success": "Éxito", + "no_update_available": "El sistema está actualizado", + "username": "Nombre de usuario" }, "diagnostics": { "cname_from_a": "Valor derivado del registro A / AAAA. Esto es permitido siempre que el registro apunte al recurso correcto.", @@ -367,7 +589,8 @@ "dns_records_name": "Nombre", "dns_records_status": "Información actual", "dns_records_type": "Tipo", - "optional": "Este récord es opcional." + "optional": "Este récord es opcional.", + "dns_records_docs": "Consulte también la documentación." }, "edit": { "active": "Activo", @@ -435,13 +658,79 @@ "title": "Editar objeto", "unchanged_if_empty": "Si no hay cambios dejalo en blanco", "username": "Nombre de usuario", - "validate_save": "Validar y guardar" + "validate_save": "Validar y guardar", + "app_passwd_protocols": "Protocolos permitidos con contraseña de aplicación", + "domain_footer_info": "Los pies de página de dominio se añaden a todos los mensajes salientes remitidos por una dirección de dicho dominio.
    Están disponibles las siguientes variables para el pie de página:", + "sender_acl_info": "Si el usuario del buzón A tiene permitido enviar como el buzón B, la dirección de remitente no se mostrará automáticamente como seleccionable en el campo \"De\" en SOGo.
    \n El usuario del buzón B necesitará crear una delegación en SOGo para permitir al usuario A seleccionar su dirección como remitente. Para delegar un buzón en SOGo, utilice el menú (tres puntos) a la derecha del nombre del buzón en la esquina superior izquierda, en la vista de correo. Este comportamiento no se aplica a direcciones alias.", + "sogo_access_info": "Tras iniciar sesión, el usuario será redirigido automáticamente a SOGo.", + "comment_info": "Un comentario privado no es visible para el usuario, mientras que un comentario público se muestra como descripción emergente al pasar el ratón en la vista general del usuario", + "quota_warning_bcc_info": "Los avisos se enviarán como copias separadas a los siguientes destinatarios. Se indicará en el asunto el usuario afectado entre paréntesis, como por ejemplo: Aviso de cuota (usuario@ejemplo.com).", + "sogo_access": "Redirección directa a SOGo", + "sogo_visible_info": "Esta opción solamente afecta a objetos que puedan ser visualizados en SOGo (alias compartidos o no compartidos que apunten al menos a un buzón interno). Si se oculta, el alias no aparecerá como seleccionable en SOGo.", + "extended_sender_acl_info": "Se aconseja importar una clave de dominio DKIM, si está disponible.
    \n Recuerde añadir este servidor al registro SPF correspondiente.
    \n Siempre que se añada un dominio o alias a este servidor, que se superponga con una dirección externa, se eliminará la dirección externa.
    \n Utilice @dominio.tld para permitir enviar como *@dominio.tld.", + "pushover_info": "La configuración de notificaciones push se aplicará a todos los mensajes limpios (no spam) entregados a %s incluyendo alias (compartidos, no compartidos, etiquetados).", + "mbox_rl_info": "Este límite de peticiones se aplica al nombre de inicio de sesión SASL, coincide con cualquier dirección \"de\" que utilice el usuario conectado. Un límite de buzón tiene precedencia sobre un límite del dominio.", + "admin": "Editar administrador", + "none_inherit": "Ninguno / heredar", + "sender_acl_disabled": "Comprobación de remitente desactivada", + "footer_exclude": "Excluir del pie de página", + "acl": "ACL (permisos)", + "advanced_settings": "Configuración avanzada", + "allow_from_smtp": "Permitir únicamente a las siguientes direcciones IP utilizar SMTP", + "allow_from_smtp_info": "Dejar en blanco para permitir cualquier remitente.
    Direcciones y redes IPv4/IPv6.", + "allowed_protocols": "Protocolos permitidos para acceso directo del usuario (no afecta a protocolos con contraseña de aplicación)", + "app_name": "Nombre de aplicación", + "app_passwd": "Contraseña de aplicación", + "created_on": "Creado", + "custom_attributes": "Atributos personalizados", + "delete_ays": "Por favor, confirme el proceso de eliminación.", + "disable_login": "Deshabilitar inicio de sesión (se aceptará el correo entrante)", + "domain_footer": "Pie de página para todos los usuarios del dominio", + "domain_footer_html": "Pie de página HTML", + "domain_footer_skip_replies": "Descartar pie de página en correos de respuesta", + "extended_sender_acl": "Direcciones de remisión externas", + "generate": "generar", + "lookup_mx": "El destino es una expresión regular con la que contrastar el nombre MX (.*\\.google\\.com para dirigir todo el correo enviado a un MX que termine en google.com a través de este salto)", + "mailbox_relayhost_info": "Aplicable únicamente al buzón y sus alias directos, anula el host de retransmisión para el dominio", + "mailbox_rename": "Renombrar buzón", + "mailbox_rename_agree": "He creado una copia de seguridad.", + "mailbox_rename_warning": "¡IMPORTANTE! Realice una copia de seguridad antes de renombrar el buzón.", + "mailbox_rename_alias": "Crear alias automáticamente", + "mailbox_rename_title": "Nuevo nombre de buzón local", + "password_recovery_email": "Dirección de correo para recuperación de contraseña", + "private_comment": "Comentario privado", + "public_comment": "Comentario público", + "pushover": "Pushover", + "pushover_evaluate_x_prio": "Escalar correo de alta prioridad [X-Priority: 1]", + "pushover_sender_array": "Tener en cuenta únicamente las siguientes direcciones de correo de remitente (separados por comas)", + "pushover_text": "Texto de notificación", + "pushover_title": "Título de notificación", + "pushover_sound": "Sonido", + "pushover_verify": "Verificar credenciales", + "quota_warning_bcc": "CCO de aviso de cuota", + "ratelimit": "Límite de peticiones", + "relay_transport_info": "
    Información
    Puede definir mapas de transporte para destinatarios personalizados para este dominio. Si no se establece, se realizará una búsqueda MX.", + "relay_unknown_only": "Reenviar solamente los buzones no existentes. Los buzones existentes se entregarán localmente.", + "sogo_visible": "Alias visible en SOGo.", + "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" }, "footer": { "hibp_nok": "¡Se encontró coincidencia - esta es una contraseña no segura, selecciona otra!", "hibp_ok": "No se encontraron coincidencias", "loading": "Espera por favor...", - "restart_now": "Reiniciar ahora" + "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.", + "cancel": "Cancelar", + "confirm_delete": "Confirmar eliminación", + "delete_now": "Eliminar ahora", + "delete_these_items": "Confirme sus cambios para el siguiente ID de objeto", + "hibp_check": "Comprobar en haveibeenpwned.com", + "nothing_selected": "Nada seleccionado", + "restarting_container": "Reiniciando contenedor, puede llevar un tiempo" }, "header": { "administration": "Administración", @@ -450,17 +739,38 @@ "mailcow_config": "Configuración", "quarantine": "Cuarentena", "restart_sogo": "Reiniciar SOGo", - "user_settings": "Configuraciones de usuario" + "user_settings": "Configuraciones de usuario", + "mailcow_system": "Sistema", + "apps": "Aplicaciones", + "restart_netfilter": "Reiniciar netfilter" }, "info": { "awaiting_tfa_confirmation": "En espera de confirmación de TFA", - "no_action": "No hay acción aplicable" + "no_action": "No hay acción aplicable", + "session_expires": "Su sesión expirará en unos 15 segundos" }, "login": { "delayed": "El inicio de sesión ha sido retrasado %s segundos.", "login": "Inicio de sesión", "password": "Contraseña", - "username": "Nombre de usuario" + "username": "Nombre de usuario", + "login_admin": "Inicio de sesión de administrador", + "invalid_pass_reset_token": "El token de restablecimiento de contraseña no es válido o ha caducado.
    Solicite un nuevo enlace de restablecimiento de contraseña.", + "fido2_webauthn": "Inicio de sesión FIDO2/WebAuthn", + "forgot_password": "> ¿Contraseña olvidada?", + "mobileconfig_info": "Inicie sesión como usuario de buzón para descargar el perfil de conexión solicitado para dispositivos Apple.", + "new_password": "Nueva contraseña", + "back_to_mailcow": "Volver a mailcow", + "login_linkstext": "¿Sesión incorrecta?", + "login_usertext": "Iniciar sesión como usuario", + "login_domainadmintext": "Iniciar sesión como administrador de dominio", + "login_admintext": "Iniciar sesión como administrador", + "login_user": "Inicio de sesión de usuario", + "login_dadmin": "Inicio de sesión de administrador de dominio", + "new_password_confirm": "Confirmar nueva contraseña", + "other_logins": "o iniciar sesión con", + "reset_password": "Restablecer contraseña", + "request_reset_password": "Solicitar cambio de contraseña" }, "mailbox": { "action": "Acción", @@ -572,7 +882,67 @@ "toggle_all": "Selecionar todo", "username": "Nombre de usuario", "waiting": "Esperando", - "weekly": "Cada semana" + "weekly": "Cada semana", + "sieve_preset_4": "Colocar en bandeja de entrada, omitir procesamiento posterior en filtros de sieve", + "goto_spam": "Aprender como correo no deseado", + "sieve_preset_header": "Vea los preajustes de ejemplo más abajo. Para más detalles, consulte Wikipedia.", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Nombre de usuario o contraseña incorrectos", + "tls_policy_maps_enforced_tls": "Estas políticas tendrán precedencia también para aquellos usuarios de buzones en los que sea obligatoria una conexión TLS. Si no se indica ninguna política a continuación, dichos usuarios aplicarán los valores predeterminados que se especifiquen en smtp_tls_mandatory_protocols y smtp_tls_mandatory_ciphers.", + "alias_domain_alias_hint": "Los alias no se aplican a dominios de alias automáticamente. Una dirección alias mi-alias@dominio no cubre la dirección mi-alias@dominio-alias (donde \"dominio-alias\" es un hipotético alias para el dominio \"dominio\").
    Utilice un filtro sieve para redirigir el correo a un buzón externo (ver la pestaña \"Filtros\" o utilice SOGo -> Desvío). Utilice \"expandir alias a dominio de alias\" para agregar automáticamente los alias que falten.", + "domain_templates": "Plantillas de dominio", + "sieve_preset_1": "Desechar correo con tipos de archivo probablemente peligrosos", + "created_on": "Creado", + "disable_login": "No permitir iniciar sesión (se seguirá aceptando el correo entrante)", + "mailbox": "Buzón", + "mailbox_defaults": "Ajustes por defecto", + "sogo_visible_y": "Mostrar alias en SOGo", + "spam_aliases": "Alias temporal", + "add_template": "Añadir plantilla", + "all_domains": "Todos los dominios", + "allow_from_smtp": "Permitir únicamente a estas direcciones IP utilizar SMTP", + "allow_from_smtp_info": "Dejar vacío para permitir cualquier remitente.
    Direcciones y redes IPv4/IPv6.", + "allowed_protocols": "Protocolos permitidos", + "goto_ham": "Aprender como correo deseado", + "iam": "Proveedor de identidad", + "insert_preset": "Insertar valor predeterminado de ejemplo \"%s\"", + "last_mail_login": "Último acceso al correo", + "last_pw_change": "Último cambio de contraseña", + "mailbox_defaults_info": "Definir configuración por defecto para nuevos buzones.", + "mailbox_templates": "Plantillas de buzón", + "no": "✕", + "open_logs": "Abrir registros", + "owner": "Propietario", + "private_comment": "Comentario privado", + "public_comment": "Comentario público", + "q_add_header": "al mover a carpeta de Spam", + "q_all": " al mover a carpeta de Spam y al rechazar", + "q_reject": "al rechazar", + "quarantine_category": "Categoría de notificación de cuarentena", + "recipient": "Destinatario", + "relay_unknown": "Retransmitir buzones desconocidos", + "sender": "Remitente", + "sieve_preset_2": "Marcar siempre el correo de un remitente específico como leído", + "sieve_preset_3": "Descartar silenciosamente, detener procesado de sieve", + "sieve_preset_5": "Respuesta automática (vacaciones)", + "sieve_preset_6": "Rechazar correo con respuesta", + "sieve_preset_7": "Redireccionar y guardar/descartar", + "sieve_preset_8": "Redirigir correo de un remitente específico, marcarlo como leído y clasificarlo en subcarpeta", + "sogo_visible": "Alias visible en SOGo", + "sogo_visible_n": "Ocultar alias en SOGo", + "stats": "Estadísticas", + "syncjob_check_log": "Comprobar registros", + "syncjob_last_run_result": "Resultado de la última ejecución", + "syncjob_EX_OK": "Éxito", + "syncjob_EXIT_CONNECTION_FAILURE": "Problema de conexión", + "syncjob_EXIT_TLS_FAILURE": "Problema con la conexión cifrada", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problema de autenticación", + "syncjob_EXIT_OVERQUOTA": "El buzón de destino ha superado la cuota", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "No es posible conectar con el servidor remoto", + "table_size": "Tamaño de la tabla", + "table_size_show_n": "Mostrar %s elementos", + "templates": "Plantillas", + "template": "Plantilla", + "yes": "✓" }, "oauth2": { "access_denied": "Inicie sesión como propietario del buzón para otorgar acceso a través de OAuth2.", @@ -614,7 +984,13 @@ "subj": "Asunto", "text_from_html_content": "Contenido (html convertido)", "text_plain_content": "Contenido (text/plain)", - "toggle_all": "Seleccionar todos" + "toggle_all": "Seleccionar todos", + "confirm": "Confirmar", + "deliver_inbox": "Entregar en bandeja de entrada", + "download_eml": "Descargar (.eml)", + "info": "Información", + "junk_folder": "Carpeta de correo no deseado", + "notified": "Notificado" }, "queue": { "queue_manager": "Administrador de cola" @@ -708,6 +1084,7 @@ "aliases_send_as_all": "No verificar permisos del remitente para los siguientes dominios (y sus aliases)", "change_password": "Cambiar contraseña", "create_syncjob": "Crear nuevo trabajo de sincronización", + "created_on": "Creado", "daily": "Cada día", "day": "Día", "description": "Descripción", @@ -719,6 +1096,9 @@ "edit": "Editar", "encryption": "Cifrado", "excludes": "Excluye", + "expire_in": "Expirará en", + "expire_never": "Nunca expirará", + "forever": "Siempre", "hour": "Hora", "hourly": "Cada hora", "hours": "Horas", @@ -739,7 +1119,8 @@ "shared_aliases": "Alias compartidos", "shared_aliases_desc": "Los alias compartidos no se ven afectados por la configuración específica del usuario, como el filtro de correo no deseado o la política de cifrado. Los filtros de spam correspondientes solo pueden ser realizados por un administrador como una política de dominio.", "sogo_profile_reset": "Resetear perfil SOGo", - "spam_aliases": "Alias de email temporales", + "spam_aliases": "Alias de email de spam", + "spam_aliases_info": "Un alias de spam es una dirección de correo electrónico temporal que se puede usar para proteger direcciones de correo electrónico reales.
    Opcionalmente, se puede establecer un tiempo de expiración para que el alias se desactive automáticamente después del período definido, eliminando efectivamente las direcciones abusadas o filtradas.", "spamfilter": "Filtro anti-spam", "spamfilter_behavior": "Clasificación", "spamfilter_bl": "Lista negra", @@ -773,11 +1154,55 @@ "waiting": "Esperando", "week": "Semana", "weekly": "Cada semana", - "weeks": "Semanas" + "weeks": "Semanas", + "with_app_password": "con contraseña de aplicación", + "year": "año", + "years": "años" }, "warning": { "domain_added_sogo_failed": "Se agregó el dominio pero no se pudo reiniciar SOGo, revisa los logs del servidor.", "fuzzy_learn_error": "Error aprendiendo hash: %s", - "ip_invalid": "IP inválida omitida: %s" + "ip_invalid": "IP inválida omitida: %s", + "cannot_delete_self": "No se puede eliminar el usuario conectado" + }, + "datatables": { + "collapse_all": "Contraer todo", + "aria": { + "sortAscending": ": activar para ordenar ascendentemente según la columna", + "sortDescending": ": activar para ordenar descendentemente según la columna" + }, + "infoEmpty": "Mostrando 0 a 0 de 0 apuntes", + "paginate": { + "last": "Última", + "next": "Siguiente", + "previous": "Anterior", + "first": "Primero" + }, + "processing": "Espere, por favor...", + "decimal": ".", + "emptyTable": "Sin datos disponibles en la tabla", + "expand_all": "Ampliar todo", + "info": "Mostrando apuntes _START_ a _END_ de _TOTAL_", + "infoFiltered": "(filtrado a partir de _MAX_ entradas totales)", + "thousands": ",", + "lengthMenu": "Mostrar entradas de _MENU_", + "loadingRecords": "Cargando...", + "search": "Buscar:", + "zeroRecords": "No se han encontrado registros coincidentes" + }, + "fido2": { + "set_fido2": "Registrar dispositivo FIDO2", + "set_fido2_touchid": "Registrar Touch ID en Apple M1", + "set_fn": "Establecer nombre amistoso (fácil de recordar)", + "confirm": "Confirmar", + "fido2_auth": "Iniciar sesión con FIDO2", + "fido2_success": "Dispositivo registrado con éxito", + "fido2_validation_failed": "Validación fallida", + "fn": "Nombre amistoso (fácil de recordar)", + "known_ids": "ID conocidas", + "none": "Deshabilitado", + "register_status": "Estado de registro", + "rename": "Renombrar", + "start_fido2_validation": "Iniciar validación FIDO2" } } diff --git a/mailcow/data/web/lang/lang.fr-fr.json b/mailcow/data/web/lang/lang.fr-fr.json index e9bff05..af0df47 100644 --- a/mailcow/data/web/lang/lang.fr-fr.json +++ b/mailcow/data/web/lang/lang.fr-fr.json @@ -16,7 +16,7 @@ "quarantine_notification": "Modifier la notification de quarantaine", "quarantine_category": "Modifier la catégorie de la notification de quarantaine", "ratelimit": "Limite d'envoi", - "recipient_maps": "Cartes destinataire", + "recipient_maps": "Cartes des destinataires", "smtp_ip_access": "Changer les hôtes autorisés pour SMTP", "sogo_access": "Autoriser la gestion des accès à SOGo", "sogo_profile_reset": "Réinitialiser le profil SOGo", @@ -109,7 +109,9 @@ "bcc_dest_format": "La destination Cci doit être une seule adresse de courriel valide.
    Si vous avez besoin d'envoyer une copie à plusieurs adresses, créez un alias et utilisez-le ici.", "tags": "Etiquettes", "app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application", - "dry": "Simuler la synchronisation" + "dry": "Simuler la synchronisation", + "internal": "Interne", + "internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine." }, "admin": { "access": "Accès", @@ -359,7 +361,57 @@ "service": "Service", "success": "Succès", "cors_settings": "Paramètres CORS", - "login_time": "Horodatage de connexion" + "login_time": "Horodatage de connexion", + "domainadmin_quicklink": "Masquer le lien rapide vers la page de connexion de l'administrateur du domaine", + "force_sso_text": "Si un fournisseur OIDC externe est configuré, cette option masque les formulaires de connexion par défaut de mailcow et n'affiche que le bouton Singe Sign-On", + "app_hide": "Masquer pour la connexion", + "admin_quicklink": "Masquer le lien rapide vers la page de connexion de l'administrateur", + "login_page": "Page de connexion", + "force_sso": "Désactiver la connexion mailcow et n'afficher que Singe Sign-On", + "iam_attribute_field": "Champ d'attribut", + "iam_authorize_url": "Endpoint d'autorisation", + "iam_auth_flow": "Flow d'authentification", + "iam_auth_flow_info": "Outre le flux de code d'autorisation (flux standard dans Keycloak), qui est utilisé pour la connexion unique, mailcow prend également en charge le flux d'authentification avec des informations d'identification directes. Le flux Mailpassword tente de valider les informations d'identification de l'utilisateur en utilisant l'API REST de Keycloak Admin. mailcow récupère le mot de passe haché dans l'attribut mailcow_password, qui est mappé dans Keycloak.", + "iam_basedn": "Base DN", + "iam_client_id": "ID Client", + "iam_client_secret": "Secret Client", + "iam_use_ssl": "Utiliser SSL", + "iam_redirect_url": "Url de redirection", + "iam_use_ssl_info": "Si le protocole SSL est activé et que le port est défini sur 389, il sera automatiquement remplacé par 636.", + "iam_use_tls_info": "Si vous activez TLS, vous devez utiliser le port par défaut de votre serveur LDAP (389). Les ports SSL ne peuvent pas être utilisés.", + "iam_version": "Version", + "ignore_ssl_error": "Ignorer les erreurs SSL", + "iam_login_provisioning": "Créer automatiquement l'utilisateur à la connexion", + "iam_mapping": "Mapping des attributs", + "iam_bindpass": "Lier le mot de passe", + "iam_periodic_full_sync": "Synchronisation complète périodique", + "iam_port": "Port", + "iam_realm": "Realm", + "iam_rest_flow": "Flow Mailpassword", + "iam_server_url": "URL du serveur", + "iam_sso": "Single Sign-On", + "iam_sync_interval": "Interval de Sync / Import (min)", + "iam_test_connection": "Test de connexion", + "iam_token_url": "Endpoint Token", + "iam_userinfo_url": "Endpoint User info", + "iam_username_field": "Champ du nom d'utilisateur", + "iam_binddn": "Bind DN", + "iam_use_tls": "Utiliser StartTLS", + "quicklink_text": "Afficher ou masquer les liens rapides vers d'autres pages de connexion sous le formulaire de connexion", + "task": "Tâche", + "user_link": "Lien utilisateur", + "user_quicklink": "Masquer le lien rapide vers la page de connexion de l'utilisateur", + "iam_client_scopes": "Scopes Client", + "iam_default_template": "Template par défaut", + "iam_default_template_description": "Si aucun modèle n'est attribué à un utilisateur, le modèle par défaut sera utilisé pour créer la boîte aux lettres, mais pas pour la mettre à jour.", + "iam_description": "Configurer un fournisseur externe pour l'authentification
    Les boîtes aux lettres des utilisateurs seront automatiquement créées lors de leur première connexion, à condition qu'un mappage d'attributs ait été défini.", + "iam_extra_permission": "Pour que les paramètres suivants fonctionnent, le client mailcow dans Keycloak a besoin d'un Compte de service et de l'autorisation de voir les utilisateurs.", + "iam_host": "Hôte", + "iam_host_info": "Saisissez un ou plusieurs hôtes LDAP, séparés par des virgules.", + "iam_import_users": "Importer des utilisateurs", + "filter": "Filtrer", + "needs_restart": "nécessite un redémarrage", + "iam": "Fournisseur d'identité" }, "danger": { "access_denied": "Accès refusé ou données de formulaire non valides", @@ -393,7 +445,7 @@ "global_filter_write_error": "Impossible d’écrire le fichier de filtre : %s", "global_map_invalid": "ID de carte globale %s non valide", "global_map_write_error": "Impossible d’écrire l’ID de la carte globale %s : %s", - "goto_empty": "Une adresse alias doit contenir au moins une adresse 'goto'valide", + "goto_empty": "Une adresse alias doit contenir au moins une adresse 'goto' valide", "goto_invalid": "Adresse Goto %s non valide", "ham_learn_error": "Erreur d'apprentissage Ham : %s", "imagick_exception": "Erreur : Exception Imagick lors de la lecture de l’image", @@ -496,7 +548,15 @@ "password_reset_invalid_user": "Boîte mail introuvable ou aucune adresse de récupération n'a été définie", "password_reset_na": "La réinitialisation des mots de passe est actuellement indisponible. Veuillez contacter votre administrateur.", "reset_token_limit_exceeded": "Le nombre limite de jetons de réinitialisation a été dépassé. Veuillez réessayer plus tard.", - "to_invalid": "Le destinataire ne doit pas être vide" + "to_invalid": "Le destinataire ne doit pas être vide", + "generic_server_error": "Une erreur de serveur inattendue s'est produite. Veuillez contacter votre administrateur.", + "authsource_in_use": "Le fournisseur d'identité ne peut pas être modifié ou supprimé car il est actuellement utilisé par un ou plusieurs utilisateurs.", + "iam_test_connection": "Échec de la connexion", + "required_data_missing": "La donnée requise %s est manquante", + "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" }, "debug": { "chart_this_server": "Graphique (ce serveur)", @@ -553,7 +613,7 @@ "alias": "Éditer les alias", "allow_from_smtp": "Restreindre l'utilisation de SMTP à ces adresses IP", "allow_from_smtp_info": "Laissez vide pour autoriser tous les expéditeurs.
    Adresses IPv4/IPv6 et réseaux.", - "allowed_protocols": "Protocoles autorisés", + "allowed_protocols": "Protocoles autorisés pour l'accès direct de l'utilisateur (n'affecte pas les protocoles de mot de passe de l'application)", "app_name": "Nom de l'application", "app_passwd": "Mot de passe de l'application", "automap": "Tenter de cibler automatiquement les dossiers (« Sent items », « Sent » => « Sent » etc.)", @@ -641,7 +701,7 @@ "spam_score": "Définir un score spam personnalisé", "subfolder2": "Synchronisation dans le sous-dossier sur la destination
    (vide = ne pas utiliser de sous-dossier)", "syncjob": "Modifier la tâche de synchronisation", - "target_address": "Adresse(s) Goto(séparé(s) par des virgules)", + "target_address": "Adresse(s) Goto (séparé(s) par des virgules)", "target_domain": "Domaine cible", "timeout1": "Délai de connexion à l’hôte distant", "timeout2": "Délai de connexion à l’hôte local", @@ -672,7 +732,7 @@ "none_inherit": "Aucun / Héritage", "quota_warning_bcc": "Avertissement sur les quotas BCC", "quota_warning_bcc_info": "Les avertissements seront envoyés en copies séparées aux destinataires suivants. Le sujet sera précédé du nom d'utilisateur correspondant entre parenthèses, par exemple : Avertissement sur les quotas (user@example.com).", - "sogo_access_info": "L'authentification unique à partir de l'interface de messagerie reste opérationnelle. Ce paramètre n'affecte pas l'accès à tous les autres services et ne supprime ni, ne modifie le profil SOGo existant d'un utilisateur.", + "sogo_access_info": "Après s'être connecté, l'utilisateur est automatiquement redirigé vers SOGo.", "admin": "Modifier l'administrateur", "password_recovery_email": "Adresse email de récupération", "mailbox_rename_title": "Nouveau nom de la partie locale de la boîte de réception", @@ -680,9 +740,22 @@ "mailbox_rename_agree": "J'ai fait une sauvegarde.", "mailbox_rename_warning": "IMPORTANT ! Faites une sauvegarde avant de renommer la boîte de réception.", "mailbox_rename_alias": "Créer un alias automatiquement", - "sogo_access": "Autoriser la connexion directe à SOGo", + "sogo_access": "Redirection directe vers SOGo", "pushover": "Pushover", - "pushover_sound": "Son" + "pushover_sound": "Son", + "internal": "Interne", + "internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine.", + "mta_sts": "MTA-STS", + "mta_sts_version": "Version", + "mta_sts_version_info": "Défini la version du standard MTA-STS – actuellement seul STSv1 est valide.", + "mta_sts_mode": "Mode", + "mta_sts_max_age": "Âge maximum", + "mta_sts_mx": "Serveur MX", + "mta_sts_mx_notice": "Plusieurs serveurs MX peuvent être spécifiés (séparés par des virgules).", + "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)." }, "footer": { "cancel": "Annuler", @@ -730,7 +803,15 @@ "new_password": "Nouveau mot de passe", "new_password_confirm": "Confirmer le nouveau mot de passe", "reset_password": "Réinitialiser le mot de passe", - "request_reset_password": "Demander le changement du mot de passe" + "request_reset_password": "Demander le changement du mot de passe", + "login_user": "Connexion Utilisateur", + "login_dadmin": "Connexion Administrateur de domaine", + "login_admin": "Connexion Administrateur", + "login_linkstext": "L'identifiant n'est pas correct ?", + "login_usertext": "Se connecter en tant qu'utilisateur", + "login_domainadmintext": "Se connecter en tant qu'administrateur du domaine", + "login_admintext": "Se connecter en tant qu'administrateur", + "email": "Adresse de courriel" }, "mailbox": { "action": "Action", @@ -836,8 +917,8 @@ "recipient_map_new": "Nouveau destinataire", "recipient_map_new_info": "La destination de la carte du destinataire doit être une adresse de courriel valide ou un nom de domaine.", "recipient_map_old": "Destinataire original", - "recipient_map_old_info": "Une carte de destination originale doit être une adresse de courriel valide ou un nom de domaine.", - "recipient_maps": "Cartes des bénéficiaires", + "recipient_map_old_info": "La destination originale des cartes des destinataires doit être une adresse de courriel valide ou un nom de domaine.", + "recipient_maps": "Cartes des destinataires", "relay_all": "Relayer tous les destinataires", "remove": "Supprimer", "resources": "Ressources", @@ -905,7 +986,9 @@ "template": "Modèle", "syncjob_check_log": "Vérifier le journal", "recipient": "Destinataire", - "open_logs": "Afficher les journaux" + "open_logs": "Afficher les journaux", + "iam": "Fournisseur d'identité", + "internal": "Interne" }, "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.", @@ -1076,7 +1159,9 @@ "recovery_email_sent": "Email de réinitialisation envoyé à %s", "mailbox_renamed": "La boîte de réception a été renommée de %s à %s", "template_modified": "Les modifications au modèle %s ont été enregistrées", - "password_policy_saved": "La politique de mot de passe a été enregistrée avec succès" + "password_policy_saved": "La politique de mot de passe a été enregistrée avec succès", + "custom_login_modified": "La personnalisation de la connexion a été sauvegardée avec succès", + "iam_test_connection": "Connexion réussie" }, "tfa": { "api_register": "%s utilise l'API Yubico Cloud. Veuillez obtenir une clé API pour votre clé ici", @@ -1160,7 +1245,7 @@ "email_and_dav": "Courriel, calendriers et contacts", "encryption": "Chiffrement", "excludes": "Exclus", - "expire_in": "Expire dans", + "expire_in": "Expirer dans", "force_pw_update": "Vous devez définir un nouveau mot de passe pour pouvoir accéder aux services liés aux logiciels de groupe.", "generate": "générer", "hour": "heure", @@ -1256,8 +1341,8 @@ "with_app_password": "avec le mot de passe de l'application", "apple_connection_profile_with_app_password": "Un nouveau mot de passe est généré et ajouté au profil, de sorte qu'aucun mot de passe ne doit être saisi lors de la configuration de votre appareil. Ne partagez pas le fichier car il vous donne un accès complet à votre boîte de réception.", "attribute": "Attribut", - "direct_protocol_access": "Cet utilisateur de la boîte aux lettres dispose d'un accès externe direct aux protocoles et applications suivants. Votre administrateur contrôle ce paramètre. Il est possible de créer des mots de passe d'application pour accorder l'accès à des protocoles et des applications individuels.
    Le bouton « Connexion au webmail » permet une connexion unique à SOGo et est toujours disponible.", - "open_webmail_sso": "Connexion au webmail", + "direct_protocol_access": "Cet utilisateur de la boîte aux lettres dispose d'un accès externe direct aux protocoles et applications suivants. Votre administrateur contrôle ce paramètre. Il est possible de créer des mots de passe d'application pour accorder l'accès à des protocoles et des applications individuels.
    Le bouton « Connexion au Webmail » permet une connexion unique à SOGo et est toujours disponible.", + "open_webmail_sso": "Connexion au Webmail", "recent_successful_connections": "Voir les connexions réussies", "syncjob_EXIT_TLS_FAILURE": "Problème de connexion chiffrée", "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problème d'authentification", @@ -1286,7 +1371,14 @@ "open_logs": "Afficher les journaux", "pushover_sound": "Son", "mailbox_general": "Général", - "mailbox_settings": "Paramètres" + "mailbox_settings": "Paramètres", + "tfa_info": "L'authentification à deux facteurs permet de protéger votre compte. Si vous l'activez, vous aurez besoin de mots de passe d'application pour vous connecter à des applications ou des services qui ne prennent pas en charge l'authentification à deux facteurs (par exemple les clients e-mails).", + "overview": "Vue d'ensemble", + "expire_never": "Ne jamais expirer", + "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" }, "warning": { "cannot_delete_self": "Impossible de supprimer l’utilisateur connecté", diff --git a/mailcow/data/web/lang/lang.gr-gr.json b/mailcow/data/web/lang/lang.gr-gr.json index df9127a..a070ea8 100644 --- a/mailcow/data/web/lang/lang.gr-gr.json +++ b/mailcow/data/web/lang/lang.gr-gr.json @@ -6,7 +6,8 @@ "weeks": "Εβδομάδες", "with_app_password": "με κωδικό εφαρμογής", "year": "χρόνος", - "years": "χρόνια" + "years": "χρόνια", + "value": "Τιμή" }, "warning": { "cannot_delete_self": "Αδυναμία διαγραφής συνδεδεμένου χρήστη", @@ -16,5 +17,170 @@ "hash_not_found": "Η κατακερματισμένη τιμή (hash value) δεν βρέθηκε ή έχει είδη διαγραφεί.", "ip_invalid": "Παραλείφθηκε μη έγκυρη διεύθυνση IP: %s", "is_not_primary_alias": "Παραλείφθηκε μη πρωτεύον ψευδώνυμο %s" + }, + "acl": { + "alias_domains": "Προσθήκη ψευδωνύμων τομέων", + "app_passwds": "Διαχείριση κωδικών εφαρμογής", + "bcc_maps": "χαρτογράφηση BCC", + "delimiter_action": "Ενέργεια οριοθέτη", + "domain_desc": "Αλλαγή περιγραφής τομέα", + "domain_relayhost": "Αλλαγή του διακομιστή αναμετάδοσης για ένα τομέα", + "eas_reset": "Επαναφορά συσκευών EAS", + "extend_sender_acl": "Να επιτρέπεται η επέκταση ACL του αποστολέα με εξωτερικές διευθύνσεις", + "filters": "Φίλτρα", + "login_as": "Είσοδος ως χρήστης e-mail", + "mailbox_relayhost": "Αλλαγή διακομιστή αναμετάδοσης για ένα γραμματοκιβώτιο", + "prohibited": "Απαγορεύεται από την ACL", + "protocol_access": "Αλλαγή πρόσβασης πρωτοκόλλου", + "pushover": "Pushover", + "pw_reset": "Επιτρέψτε την επαναφορά κωδικού πρόσβασης του χρήστη", + "quarantine": "Ενέργειες καραντίνας", + "quarantine_attachments": "Συνημμένα καραντίνας", + "quarantine_category": "Αλλαγή κατηγορίας ειδοποιήσεων καραντίνας", + "quarantine_notification": "Αλλαγή ειδοποιήσεων καραντίνας", + "ratelimit": "Όριο τιμής", + "recipient_maps": "Χάρτες παραληπτών", + "smtp_ip_access": "Αλλαγή επιτρεπόμενων διακομιστών SMTP", + "sogo_access": "Επιτρέψτε τη διαχείριση της πρόσβασης στο SOGo", + "sogo_profile_reset": "Επαναφορά του προφίλ SOGo", + "spam_alias": "Προσωρινά ψευδώνυμα", + "spam_policy": "Λίστα απορρίψεων/Λίστα επιτρεπόμενων", + "spam_score": "Βαθμολογία ανεπιθύμητης αλληλογραφίας", + "syncjobs": "Εργασίες συγχρονισμού", + "tls_policy": "Πολιτική TLS", + "unlimited_quota": "Απεριόριστο όριο για γραμματοκιβώτια" + }, + "add": { + "activate_filter_warn": "Όλα τα άλλα φίλτρα θα απενεργοποιηθούν, όταν επιλεγεί η επιλογή \"ενεργό\".", + "active": "Ενεργό", + "add": "Προσθήκη", + "add_domain_only": "Προσθήκη μόνο του τομέα", + "add_domain_restart": "Προσθήκη του τομέα και επανεκκίνηση του SOGo", + "alias_address": "Διευθύνσεις ψευδωνύμων", + "alias_address_info": "Πλήρης διεύθυνση(εις) e-mail ή @example.com, για να λαμβάνετε ΟΛΑ τα μηνύματα ενός τομέα (χωρισμένα με κόμα). μόνο τομείς του mailcow.", + "alias_domain": "Ψευδώνυμο τομέα", + "alias_domain_info": "Μόνο έγκυρα ονόματα τομέα (χωρισμένα με κόμα).", + "app_name": "Όνομα εφαρμογής", + "app_password": "Προσθήκη κωδικού εφαρμογής", + "app_passwd_protocols": "Επιτρεπόμενα πρωτόκολλα για κωδικούς εφαρμογών", + "automap": "Αυτόματη αντιστοίχηση φακέλων (\"Απεσταλμένα μηνύματα\", \"Απεσταλμένα\" => \"Στάλθηκαν\" κ.τ.λ.)", + "backup_mx_options": "Επιλογές αναμετάδοσης", + "bcc_dest_format": "Η BCC διεύθυνση πρέπει να είναι μία και έγκυρη διεύθυνση e-mail.
    Αν θέλετε να στείλετε αντίγραφα σε πολλούς παραλήπτες, δημιουργήστε ένα ψευδόνυμο για όλους και χρησιμοποιήστε το εδώ.", + "comment_info": "Τα προσωπικά σχόλια δεν είναι ορατά στον χρήστη. Τα δημόσια σχόλια εμφανίζονται ως tooltips.", + "custom_params": "Προσαρμοσμένες παράμετροι", + "custom_params_hint": "Σωστή σύνταξη: --param=xy, λάθος σύνταξη: --param xy", + "delete1": "Διαγραφή όταν ολοκληρωθεί", + "delete2": "Διαγραφή μηνυμάτων στον προορισμό που δεν βρίσκονται στην πηγή", + "delete2duplicates": "Διαγραφή διπλότυπων στον προορισμό", + "description": "Περιγραφή", + "destination": "Προορισμός", + "disable_login": "Απαγόρευση εισόδου (η εισερχόμενη αλληλογραφία εξακολουθεί να γίνεται δεκτή)", + "domain": "Τομέας", + "domain_matches_hostname": "Ο τομέας %s είναι ο ίδιος με το όνομα του διακομιστή", + "domain_quota_m": "Συνολικό όριο τομέα (MiB)", + "dry": "Προσομοίωση συγχρονισμού", + "enc_method": "Μέθοδος κρυπτογράφησης", + "exclude": "Εξαίρεση αντικειμένων (regex)", + "full_name": "Πλήρες όνομα", + "gal": "Κοινόχρηστη λίστα διευθύνσεων" + }, + "danger": { + "unknown": "Παρουσιάστηκε κάποιο άγωνστο σφάλμα", + "unknown_tfa_method": "Άγνωστη μέθοδος TFA", + "unlimited_quota_acl": "Το απεριόριστο όριο απαγορεύεται από την ACL", + "username_invalid": "Το όνομα χρήστη %s δεν μπορεί να χρησιμοποιηθεί", + "validity_missing": "Παρακαλώ ορίστε μία περίοδο εγκυρότητας", + "value_missing": "Παρακαλώ συμπληρώστε όλα τα δεδομένα", + "version_invalid": "Η έκδοση %s δεν είναι έγκυρη", + "yotp_verification_failed": "Η επαλήθευση μέσω Yubico OTP απέτυχε: %s" + }, + "datatables": { + "collapse_all": "Σύμπτυξη όλων", + "decimal": ".", + "emptyTable": "Δεν υπάρχουν εγγραφές", + "expand_all": "Επέκταση όλων", + "info": "Εμφανίζονται _START_ εώς _END_ από _TOTAL_ εγγραφές", + "infoEmpty": "Εμφανίζονται 0 εώς 0 από 0 εγγραφές", + "infoFiltered": "(φιλτραρισμένες από _MAX_ συνολικές εγγραφές)", + "thousands": ",", + "lengthMenu": "Εμφάνιση _MENU_ εγγραφών", + "loadingRecords": "Γίνεται φόρτωση...", + "processing": "Παρακαλώ περιμένετε...", + "search": "Αναζήτηση:", + "zeroRecords": "Δε βρέθηκαν εγγραφές", + "paginate": { + "first": "Πρώτη", + "last": "Τελευταία", + "next": "Επόμενη", + "previous": "Προηγούμενη" + }, + "aria": { + "sortAscending": ": ενεργοποίηση αύξουσας ταξινόμησης", + "sortDescending": ": ενεργοποίηση φθίνουσας ταξινόμησης" + } + }, + "debug": { + "architecture": "Αρχιτεκτονική", + "chart_this_server": "Γράφημα (αυτός ο διακομιστής)", + "containers_info": "Πληροφορίες για τον container", + "container_running": "Εκτελείται", + "container_disabled": "Ο container έχει σταματήσει ή απενεργοποιηθεί", + "container_stopped": "Σταματημένος", + "cores": "Πυρήνες", + "current_time": "Ώρα συστήματος", + "disk_usage": "Χρήση αποθ. χώρου", + "docs": "Έγγραφα", + "error_show_ip": "Δεν είναι δυνατή η επίλυση της δημόσιας IP διεύθυνσης", + "external_logs": "Εξωτερικά αρχεία καταγραφής", + "history_all_servers": "Ιστορικό (Όλοι οι διακομιστές)", + "in_memory_logs": "Αρχεία καταγραφής στη μνήμη", + "last_modified": "Τελευταία τροποποίηση", + "log_info": "

    mailcow in-memory logs are collected in Redis lists and trimmed to LOG_LINES (%d) every minute to reduce hammering.\n
    In-memory logs are not meant to be persistent. All applications that log in-memory, also log to the Docker daemon and therefore to the default logging driver.\n
    The in-memory log type should be used for debugging minor issues with containers.

    \n

    External logs are collected via API of the given application.

    \n

    Static logs are mostly activity logs, that are not logged to the Dockerd but still need to be persistent (except for API logs).

    ", + "login_time": "Ώρα", + "logs": "Αρχεία καταγραφής", + "memory": "Μνήμη", + "online_users": "Συνδεδεμένοι χρήστες", + "restart_container": "Επανεκκίνηση", + "service": "Υπηρεσία", + "show_ip": "Εμφάνιση δημόσιας IP", + "size": "Μέγεθος", + "started_at": "Ξεκίνησε στις", + "started_on": "Ξεκίνησε στις", + "static_logs": "Στατικά αρχεία καταγραφής", + "success": "Επιτυχία", + "system_containers": "Σύστημα και Containers", + "timezone": "Ζώνη ώρας", + "uptime": "Χρόνος λειτουργίας", + "update_available": "Υπάρχει διαθέσιμη ενημέρωση", + "no_update_available": "Έχετε τη τελευταία έκδοση του συστήματος", + "update_failed": "Δεν ήταν δυνατός ο έλεγχος για ενημερώσεις", + "username": "Όνομα χρήστη", + "wip": "Currently Work in Progress" + }, + "diagnostics": { + "cname_from_a": "Value derived from A/AAAA record. This is supported as long as the record points to the correct resource.", + "dns_records": "Εγγραφές DNS", + "dns_records_24hours": "Παρακαλώ σημειώστε ότι οι αλλαγές στο DNS μπορεί να χρειαστούν μέχρι 24 ώρες για να ενημερωθούν σωστά και να εμφανιστούν σε αυτή τη σελίδα. Ο σκοπός της είναι να δείτε πως μπορείτε να ρυθμίσετε σωστά τις εγγραφές DNS και να ελέγξετε αν είναι σωστές.", + "dns_records_data": "Σωστά δεδομένα", + "dns_records_docs": "Παρακαλώ συμβουλευτείτε επίσης την τεκμηρίωση.", + "dns_records_name": "Όνομα", + "dns_records_status": "Τρέχουσα κατάσταση", + "dns_records_type": "Τύπος", + "optional": "Αυτή η εγγραφή είναι προαιρετική." + }, + "edit": { + "acl": "ACL (Δικαίωμα)", + "active": "Ενεργό", + "admin": "Επεξεργασία διαχειριστή", + "advanced_settings": "Ρυθμίσεις για προχωρημένους", + "alias": "Επεξεργασία ψευδώνυμου", + "allow_from_smtp": "Επέτρεψε μόνο σε αυτές τις IPs να χρησιμοποιήσουν το SMTP", + "allow_from_smtp_info": "Αφήστε το κενό για να επιτρέψετε όλους τους αποστολείς.
    IPv4/IPv6 διευθύνσεις και δίκτυα.", + "allowed_protocols": "Επιτρεπόμενα πρωτόκολλα για απ' ευθείας πρόσβαση από τους χρήστες (δεν επηρεάζει τα πρωτόκολλα κωδικών πρόσβασης εφαρμογής)", + "app_name": "Όνομα εφαρμογής", + "app_passwd": "Κωδικός πρόσβασης εφαρμογής", + "app_passwd_protocols": "Επιτρέπομενα πρωτόκολλα για τον κωδικό εφαρμογής", + "automap": "Αυτόματη αντιστοίχηση φακέλων (\"Απεσταλμένα μηνύματα\", \"Απεσταλμένα\" => \"Στάλθηκαν\" κ.τ.λ.)", + "backup_mx_options": "Επιλογές αναμετάδοσης" } } diff --git a/mailcow/data/web/lang/lang.hu-hu.json b/mailcow/data/web/lang/lang.hu-hu.json index 97f575a..2f78d5b 100644 --- a/mailcow/data/web/lang/lang.hu-hu.json +++ b/mailcow/data/web/lang/lang.hu-hu.json @@ -9,11 +9,11 @@ "source": "Forrás", "spamfilter": "Spam szűrő", "subject": "Tárgy", - "sys_mails": "Rendszer-üzenetek", + "sys_mails": "Rendszer e-mailek", "text": "Szöveg", "time": "Idő", "title": "Cím", - "title_name": "\"mailcow UI\" website címe", + "title_name": "\"mailcow UI\" weboldal címe", "to_top": "Vissza a tetejére", "transport_dest_format": "Szintaxis: pelda.hu, .pelda.hu, *, fiok@pelda.hu (több érték esetén vesszővel elválasztva)", "upload": "Feltöltés", @@ -29,14 +29,14 @@ "logo_dark_label": "Sötét üzemmódhoz invertálva", "f2b_regex_info": "Figyelembe vett naplók: SOGo, Postfix, Dovecot, PHP-FPM.", "f2b_retry_window": "Újrapróbálkozási ablak (s) a maximális próbálkozásokhoz", - "f2b_whitelist": "Fehérlistás hálózatok/hostok", + "f2b_whitelist": "Engedélyezési listás hálózatok/hostok", "filter_table": "Szűrő táblázat", "forwarding_hosts": "Továbbító hostok", - "forwarding_hosts_add_hint": "Megadhat IPv4/IPv6 címeket, hálózatokat CIDR jelölésben, állomásneveket (amelyek IP-címekre lesznek feloldva) vagy tartományneveket (amelyek IP-címekre lesznek feloldva az SPF rekordok vagy ezek hiányában az MX rekordok lekérdezésével).", + "forwarding_hosts_add_hint": "Megadhat IPv4/IPv6 címeket, hálózatokat CIDR jelölésben, állomásneveket (amelyek IP címekre lesznek feloldva) vagy tartományneveket (amelyek IP címekre lesznek feloldva az SPF rekordok vagy ezek hiányában az MX rekordok lekérdezésével).", "from": "A címről", "copy_to_clipboard": "Szöveg másolva a vágólapra!", "f2b_manage_external": "A Fail2Ban külső kezelése", - "f2b_manage_external_info": "A Fail2ban továbbra is karbantartja a tiltólistát, de nem állít be aktívan szabályokat a forgalom blokkolására. Használja az alábbi generált tiltólistát a forgalom külső blokkolásához.", + "f2b_manage_external_info": "A Fail2ban továbbra is karbantartja a tiltólistát, de nem állít be aktívan szabályokat a forgalom blokkolásához. Használja az alábbi generált tiltólistát a forgalom külső blokkolásához.", "f2b_max_ban_time": "Maximális tiltási idő (s)", "f2b_netban_ipv4": "IPv4 alhálózat mérete, amelyre tilalmat kell alkalmazni (8-32)", "f2b_netban_ipv6": "IPv6 alhálózat mérete, amelyre tilalmat kell alkalmazni (8-128)", @@ -47,13 +47,13 @@ "html": "HTML", "import": "Import", "ip_check": "IP ellenőrzés", - "ip_check_disabled": "Az IP-ellenőrzés le van tiltva. Bekapcsolhatja a következő menüpont alatt
    Rendszer > Beállítások > Opciók > Személyreszabás", + "ip_check_disabled": "Az IP ellenőrzés le van tiltva. Bekapcsolhatja a következő menüpont alatt
    Rendszer > Beállítások > Opciók > Személyreszabás", "is_mx_based": "MX alapú", "last_applied": "Utoljára alkalmazott", "link": "Link", "loading": "Kérjük, várj...", "login_time": "Bejelentkezési idő", - "f2b_list_info": "Egy feketelistán szereplő állomás vagy hálózat mindig nagyobb súlyú, mint egy fehérlistás entitás. A lista frissítései néhány másodpercig tartanak.", + "f2b_list_info": "Egy tiltólistán szereplő állomás vagy hálózat mindig nagyobb súlyú, mint egy engedélyezési listás elem. A lista frissítései néhány másodpercig tartanak.", "f2b_ban_time_increment": "A tiltási idő minden egyes tiltással növekszik", "additional_rows": " - további sorokat adtak hozzá", "admin": "Adminisztrátor", @@ -68,7 +68,7 @@ "api_key": "API-kulcs", "api_read_only": "Csak olvasható hozzáférés", "api_read_write": "Olvasás-írás hozzáférés", - "api_skip_ip_check": "IP-ellenőrzés kihagyása az API esetében", + "api_skip_ip_check": "IP ellenőrzés kihagyása az API esetében", "cors_settings": "CORS beállítások", "guid": "GUID - egyedi példányazonosító", "dkim_to": "A címre", @@ -82,8 +82,8 @@ "edit": "Szerkesztés", "empty": "Nincs eredmény", "excludes": "Kizárja ezeket a kedvezményezetteket", - "f2b_ban_time": "Tilalmi idő (s)", - "f2b_blacklist": "Feketelistás hálózatok/hostok", + "f2b_ban_time": "Tiltás időtartam (s)", + "f2b_blacklist": "Tiltólistás címek", "f2b_filter": "Regex szűrők", "logo_info": "A képed 40px magasságúra lesz méretezve a felső navigációs sávhoz és max. 250px szélességűre a kezdőlaphoz. A skálázható grafika használata erősen ajánlott.", "dkim_add_key": "ARC/DKIM kulcs hozzáadása", @@ -122,21 +122,187 @@ "arrival_time": "Érkezési idő (szerveridő)", "authed_user": "Azonosított felhasználó", "ays": "Biztos, hogy folytatni akarod?", - "ban_list_info": "A tiltott IP-címek listáját lásd alább: hálózat (fennmaradó tiltási idő) - [akciók].
    A tiltás feloldására várakozó IP-ket néhány másodpercen belül eltávolítjuk az aktív tiltási listáról.
    A piros címkék a feketelistán szereplő aktív állandó tiltásokat jelzik.", + "ban_list_info": "A tiltott IP címek listáját lásd alább: hálózat (fennmaradó tiltási idő) - [akciók].
    A tiltás feloldására várakozó IP-ket néhány másodpercen belül eltávolítjuk az aktív tiltási listáról.
    A piros címkék a tiltólistán szereplő aktív állandó tiltásokat jelzik.", "configuration": "Konfiguráció", "convert_html_to_text": "HTML átalakítása egyszerű szöveggé", "credentials_transport_warning": "Figyelmeztetés: Egy új közlekedési térképbejegyzés hozzáadása frissíti a hitelesítő adatokat minden olyan bejegyzéshez, amelynek a következő ugrás oszlopa megegyezik.", "customize": "Testreszabás", "destination": "Célállomás", "customer_id": "Ügyfél azonosító", - "apps_name": "\"mailcow Apps\" név" + "apps_name": "\"mailcow Apps\" név", + "admin_quicklink": "Gyorshivatkozás elrejtése az Adminisztrátori bejelentkezési oldalra", + "app_hide": "Elrejtés a bejelentkezéshez", + "domainadmin_quicklink": "Gyorshivatkozás elrejtése a Domain adminisztrátori bejelentkezési oldalra", + "filter": "Szűrő", + "force_sso_text": "Ha egy külső OIDC-szolgáltató van beállítva, ez az opció elrejti az alapértelmezett mailcow bejelentkezési űrlapokat, és csak az egységes bejelentkezési (Single Sign-On) gombot jeleníti meg", + "force_sso": "A mailcow bejelentkezés letiltása és csak az egységes bejelentkezés megjelenítése", + "hash_remove_info": "Egy arányszám-korlát hash eltávolítása (ha még létezik) teljesen visszaállítja a számlálóját.
    \r\n Minden hash-t egyedi szín jelöl.", + "iam": "Azonosítási szolgáltató", + "iam_attribute_field": "Attribútum mező", + "iam_authorize_url": "Engedélyezési végpont", + "iam_auth_flow": "Azonosítási folyamat", + "iam_auth_flow_info": "Az engedélyezési kód áramláson (Authorization Code Flow) kívül, amelyet az egységes bejelentkezéshez (Single-Sign On) használnak (ez a standard áramlás Keycloak-ban), a mailcow támogatja a közvetlen hitelesítő adatokkal történő hitelesítési áramlást is. A Jelszóáramlás (Mailpassword Flow) megpróbálja érvényesíteni a felhasználó hitelesítő adatait a Keycloak Admin REST API használatával. A mailcow a mailcow_password attribútumból olvassa be a hash-elt jelszót, amely a Keycloak-ban van leképezve.", + "iam_basedn": "Bázis DN", + "iam_client_id": "Kliens azonosító", + "iam_client_secret": "Kliens titok", + "iam_client_scopes": "Kliens hatókörök", + "iam_default_template": "Alapértelmezett sablon", + "iam_default_template_description": "Ha nincs sablon hozzárendelve egy felhasználóhoz, az alapértelmezett sablon lesz felhasználva a postafiók létrehozásához, de a postafiók frissítéséhez nem.", + "iam_description": "Külső azonosítási szolgáltató konfigurálása.
    A felhasználók postafiókjai automatikusan létrejönnek az első bejelentkezéskor, feltéve, hogy van attribútum-hozzárendelés beállítva.", + "iam_extra_permission": "A következő beállításokhoz a Keycloak-ban lévő mailcow kliensnek szüksége van egy Service account-ra és a view-users engedélyre.", + "iam_host": "Házigazda", + "iam_host_info": "Adjon meg egy vagy több LDAP állomást, vesszővel elválasztva.", + "iam_import_users": "Felhasználók importálása", + "iam_login_provisioning": "Felhasználók automatikus létrehozása bejelentkezéskor", + "iam_mapping": "Attribútum-hozzárendelés", + "iam_bindpass": "Bind jelszó", + "iam_periodic_full_sync": "Időszakos teljes szinkronizálás", + "iam_port": "Port", + "iam_realm": "Tartomány", + "iam_redirect_url": "Átirányítási URL", + "iam_rest_flow": "Mailpassword Flow", + "iam_server_url": "Szerver URL", + "iam_sso": "Egységes bejelentkezés", + "iam_sync_interval": "Szinkronizálási / importálási időtartam (perc)", + "iam_test_connection": "Kapcsolat tesztelése", + "iam_token_url": "Token végpont", + "iam_userinfo_url": "Felhasználói információ végpont", + "iam_username_field": "Felhasználónév mező", + "iam_binddn": "Bind DN", + "iam_use_ssl": "SSL használata", + "iam_use_ssl_info": "Ha az SSL-t engedélyezi, és a port 389-re van állítva, az automatikusan felülíródik 636-ra.", + "iam_use_tls": "StartTLS használata", + "iam_use_tls_info": "Ha a TLS-t engedélyezi, az LDAP szerver alapértelmezett portját kell használnia (389). Az SSL portok nem használhatók.", + "iam_version": "Verzió", + "ignore_ssl_error": "SSL hibák figyelmen kívül hagyása", + "ip_check_opt_in": "Választás a harmadik féltől származó ipv4.mailcow.email és ipv6.mailcow.email szolgáltatás használatára a külső IP címek feloldásához.", + "license_info": "A licensz nem kötelező, de segít a további fejlesztésben.
    Regisztrálja a GUID-ját itt vagy vásároljon támogatást a mailcow telepítéséhez.", + "lookup_mx": "A cél egy reguláris kifejezés, amely illeszkedik az MX névre (.*\\.google\\.com, hogy a google.com-ra végződő MX-en keresztül továbbítsa az összes levelet)", + "main_name": "\"mailcow UI\" név", + "merged_vars_hint": "A szürkével jelölt sorok a vars.(local.)inc.php fájlból lettek összevonva, és nem módosíthatók.", + "message": "Üzenet", + "message_size": "Üzenet mérete", + "nexthop": "Következő ugrás", + "no": "✕", + "no_active_bans": "Nincs aktív tiltás", + "no_new_rows": "Nincs több sor elérhető", + "no_record": "Nincs bejegyzés", + "oauth2_apps": "OAuth2 alkalmazások", + "oauth2_add_client": "OAuth2 kliens hozzáadása", + "oauth2_client_id": "Kliens azonosító", + "oauth2_client_secret": "Kliens titok", + "oauth2_info": "Az OAuth2 implementáció támogatja az \"Authorization Code\" engedélyezési típust és frissítési tokeneket ad ki.
    \r\nA szerver automatikusan új frissítési tokeneket is kiad, miután egy frissítési token fel lett használva.

    \r\n• Az alapértelmezett hatókör a profile. Csak postafiók-felhasználók hitelesíthetők az OAuth2-vel szemben. Ha a scope paraméter elmarad, az visszaesik a profile-ra.
    \r\n• A state paramétert a kliensnek kötelező elküldenie az engedélyezési kérelem részeként.

    \r\nAz OAuth2 API-hoz intézett kérések útvonalai:
    \r\n
      \r\n
    • Engedélyezési végpont: /oauth/authorize
    • \r\n
    • Token végpont: /oauth/token
    • \r\n
    • Erőforrás oldal: /oauth/profile
    • \r\n
    \r\nA kliens titok újragenerálása nem jár le a meglévő engedélyezési kódok lejáratával, de azok nem tudják megújítani a tokenjüket.

    \r\nA kliens tokenek visszavonása az összes aktív munkamenet azonnali leállítását okozza. Minden kliensnek újra kell hitelesítenie magát.", + "oauth2_redirect_uri": "Átirányítási URI", + "oauth2_renew_secret": "Új kliens titok generálása", + "oauth2_revoke_tokens": "Minden kliens token visszavonása", + "optional": "opcionális", + "options": "Opciók", + "password": "Jelszó", + "password_length": "Jelszó hossza", + "password_policy": "Jelszó házirend", + "password_policy_chars": "Legalább egy betűkaraktert kell tartalmaznia", + "password_policy_length": "A jelszó minimális hossza %d", + "password_policy_lowerupper": "Kis- és nagybetűket kell tartalmaznia", + "password_policy_numbers": "Legalább egy számot kell tartalmaznia", + "password_policy_special_chars": "Speciális karaktereket kell tartalmaznia", + "password_repeat": "Megerősítő jelszó (ismétlés)", + "password_reset_info": "Ha nincs megadva helyreállítási e-mail cím, ez a funkció nem használható.", + "password_reset_settings": "Jelszó-helyreállítási beállítások", + "password_reset_tmpl_html": "HTML sablon", + "password_reset_tmpl_text": "Szöveges sablon", + "password_settings": "Jelszó beállítások", + "priority": "Prioritás", + "private_key": "Privát kulcs", + "quarantine": "Karantén", + "quarantine_bcc": "Az összes értesítés másolatának elküldése (BCC) erre a címzettnek:
    Hagyja üresen a letiltáshoz. Aláíratlan, nem ellenőrzött levél. Csak belső kézbesítésre alkalmas.", + "quarantine_exclude_domains": "Tartományok és alias-tartományok kizárása", + "quarantine_max_age": "Maximális életkor napokban
    Az értéknek legalább 1 napnak kell lennie.", + "quarantine_max_score": "Az értesítés elvetése, ha egy levél spam pontszáma magasabb ennél az értéknél:
    Alapértelmezett értéke 9999.0", + "quarantine_max_size": "Maximális méret MiB-ban (a nagyobb elemeket elveti):
    A 0 nem jelent korlátlant.", + "quarantine_notification_html": "Értesítő e-mail sablon:
    Hagyja üresen az alapértelmezett sablon visszaállításához.", + "quarantine_notification_sender": "Értesítő e-mail feladója", + "quarantine_notification_subject": "Értesítő e-mail tárgya", + "quarantine_redirect": "Az összes értesítés átirányítása erre a címzettnek:
    Hagyja üresen a letiltáshoz. Aláíratlan, nem ellenőrzött levél. Csak belső kézbesítésre alkalmas.", + "quarantine_release_format": "A feloldott elemek formátuma", + "quarantine_release_format_att": "Csatolmányként", + "quarantine_release_format_raw": "Eredeti, módosítatlan formában", + "quarantine_retention_size": "Megőrzések postafiókonként:
    A 0 inaktívat jelent.", + "quicklink_text": "A gyorshivatkozások megjelenítése vagy elrejtése a bejelentkezési űrlap alatt", + "quota_notification_html": "Értesítő e-mail sablon:
    Hagyja üresen az alapértelmezett sablon visszaállításához.", + "quota_notification_sender": "Kvóta értesítő e-mail feladója", + "quota_notification_subject": "Kvóta értesítő e-mail tárgya", + "quota_notifications": "Kvóta értesítések", + "quota_notifications_info": "A kvóta értesítéseket a felhasználók egyszer kapják meg, amikor a 80%-os határt, majd a 95%-os határt átlépik.", + "quota_notifications_vars": "{{percent}} a felhasználó aktuális kvótáját jelenti
    {{username}} a postafiók neve", + "queue_unban": "tiltás feloldása", + "r_active": "Aktív korlátozások", + "r_inactive": "Inaktív korlátozások", + "r_info": "A szürkével/letiltva jelölt elemek az aktív korlátozások listáján a mailcow számára nem érvényes korlátozások, és nem mozgathatók. Az ismeretlen korlátozások a megjelenési sorrendben lesznek beállítva.
    Új elemeket adhat hozzá a inc/vars.local.inc.php fájlban, hogy váltani tudja őket.", + "rate_name": "Arányszám neve", + "recipients": "Címzettek", + "refresh": "Frissítés", + "regen_api_key": "API kulcs újragenerálása", + "regex_maps": "Regex térképek", + "relay_from": "\"Feladó:\" cím", + "relay_rcpt": "\"Címzett:\" cím", + "relay_run": "Teszt futtatása", + "relayhosts": "Feladófüggő szállítások", + "relayhosts_hint": "Határozzon meg feladófüggő szállításokat, hogy kiválaszthassa őket egy tartomány konfigurációs párbeszédpanelén.
    \r\n A szállítási szolgáltatás mindig \"smtp:\" és ezért megpróbálja a TLS-t, ha felajánlják. A becsomagolt TLS (SMTPS) nem támogatott. A felhasználók egyéni kimenő TLS-szabályzati beállításai figyelembe vételre kerülnek.
    \r\n Érintik a kiválasztott tartományokat, beleértve az alias tartományokat is.", + "remove": "Eltávolítás", + "remove_row": "Sor eltávolítása", + "reset_default": "Visszaállítás alapértelmezettre", + "reset_limit": "Hash eltávolítása", + "reset_password_vars": "{{link}} A generált jelszó-helyreállítási link
    {{username}} A jelszó-helyreállítást kérő felhasználó postafiók neve
    {{username2}} A helyreállítási postafiók neve
    {{date}} A jelszó-helyreállítási kérelem dátuma
    {{token_lifetime}} A token élettartama percekben
    {{hostname}} A mailcow hosztnév", + "restore_template": "Hagyja üresen az alapértelmezett sablon visszaállításához.", + "routing": "Útválasztás", + "rsetting_add_rule": "Szabály hozzáadása", + "rsetting_content": "Szabály tartalma", + "rsetting_desc": "Rövid leírás", + "rsetting_no_selection": "Kérjük, válasszon egy szabályt", + "rsetting_none": "Nincsenek elérhető szabályok", + "rsettings_insert_preset": "Példa előre beállított \"%s\" beillesztése", + "rsettings_preset_1": "Minden letiltása, kivéve a DKIM és az arányszám-korlátot a hitelesített felhasználók számára", + "rsettings_preset_2": "A Postmasterek spamet akarnak", + "rsettings_preset_3": "Csak bizonyos feladókat engedélyezzen egy postafiók számára (pl. csak belső postafiókként használva)", + "rsettings_preset_4": "Rspamd letiltása egy tartomány számára", + "rspamd_com_settings": "A beállítás neve automatikusan generálódik, kérjük, nézze meg a lenti példa-előrebeállításokat. További részletekért lásd a Rspamd dokumentációját", + "rspamd_global_filters": "Globális szűrőtérképek", + "rspamd_global_filters_agree": "Óvatos leszek!", + "rspamd_global_filters_info": "A globális szűrőtérképek különböző típusú globális tiltó- és engedélyezési listákat tartalmaznak.", + "rspamd_global_filters_regex": "A nevük elmagyarázza a céljukat. Minden tartalomnak érvényes reguláris kifejezést kell tartalmaznia \"/pattern/options\" formátumban (pl. /.+@domain\\.tld/i).
    \r\n Bár alapvető ellenőrzések történnek minden regex soron, az Rspamd funkcionalitása megszakadhat, ha nem sikerül helyesen értelmeznie a szintaxist.
    \r\n Az Rspamd megpróbálja elolvasni a térkép tartalmát, amikor az megváltozik. Ha problémákat tapasztal, indítsa újra az Rspamd-et, hogy kikényszerítse a térkép újratöltését.
    A tiltólistára került elemek ki vannak zárva a karanténból.", + "rspamd_settings_map": "Rspamd beállítási térkép", + "sal_level": "Moo szint", + "service": "Szolgáltatás", + "service_id": "Szolgáltatás azonosító", + "task": "Feladat", + "transport_maps": "Szállítási térképek", + "transport_test_rcpt_info": "• Használja a null@hosted.mailcow.de címet a külföldi célra történő továbbítás teszteléséhez.", + "transports_hint": "• Egy szállítási térkép bejegyzés felülírja a feladófüggő szállítási térképet.
    \r\n• Az MX-alapú szállítások előnyben részesítettek.
    \r\n• A felhasználónkénti kimenő TLS-szabályzati beállítások figyelmen kívül hagyódnak, és csak a TLS-szabályzati térkép bejegyzései kényszeríthetik ki őket.
    \r\n• A definiált szállításokhoz a szállítási szolgáltatás mindig \"smtp:\" és ezért megpróbálja a TLS-t, ha felajánlják. A becsomagolt TLS (SMTPS) nem támogatott.
    \r\n• A \"/localhost$/\"-nek megfelelő címek mindig \"local:\"-on keresztül lesznek szállítva, ezért a \"*\" cél nem vonatkozik ezekre a címekre.
    \r\n• A hitelesítő adatok meghatározásához egy példa következő ugráshoz \"[host]:25\", a Postfix mindig lekérdezi a \"host\"-ot, mielőtt a \"[host]:25\"-re keresne. Ez a viselkedés lehetetlenné teszi a \"host\" és a \"[host]:25\" egyidejű használatát.", + "ui_footer": "Lábléc (HTML engedélyezett)", + "ui_header_announcement": "Közlemények", + "ui_header_announcement_active": "Közlemény beállítása aktívra", + "ui_header_announcement_content": "Szöveg (HTML engedélyezett)", + "ui_header_announcement_help": "A közlemény minden bejelentkezett felhasználó számára látható, valamint a felhasználói felület bejelentkezési képernyőjén.", + "ui_header_announcement_select": "Válassza ki a közlemény típusát", + "ui_header_announcement_type": "Típus", + "ui_header_announcement_type_danger": "Nagyon fontos", + "ui_header_announcement_type_info": "Információ", + "ui_header_announcement_type_warning": "Fontos", + "ui_texts": "Felhasználói felület címkéi és szövegei", + "unban_pending": "tiltás feloldása függőben", + "unchanged_if_empty": "Ha nem változik, hagyja üresen", + "user_link": "Felhasználói-link", + "user_quicklink": "Gyorshivatkozás elrejtése a Felhasználói bejelentkezési oldalra", + "validate_license_now": "GUID érvényesítése a licenszszerverrel szemben", + "yes": "✓", + "success": "Siker" }, "edit": { "active": "Aktív", "advanced_settings": "Haladó beállítások", "alias": "Alias szerkesztése", - "allow_from_smtp": "Kizárólag ezen IP-címek használhatnak SMTP-t", - "allow_from_smtp_info": "Leave empty to allow all senders.
    IPv4/IPv6 addresses and networks.", + "allow_from_smtp": "Kizárólag ezen IP címek használhatnak SMTP-t", + "allow_from_smtp_info": "Hagyja üresen minden feladó engedélyezéséhez.
    IPv4/IPv6 címek és hálózatok.", "allowed_protocols": "Engedélyezett protokollok", "app_name": "Applikáció neve", "app_passwd": "Applikáció jelszava", @@ -146,7 +312,127 @@ "description": "Leírás", "domain_quota": "Domain kvóta", "domains": "Domainek", - "edit_alias_domain": "Alias domain szerkesztése" + "edit_alias_domain": "Alias domain szerkesztése", + "acl": "ACL (Engedélyek)", + "admin": "Adminisztrátor szerkesztése", + "app_passwd_protocols": "Engedélyezett protokollok az alkalmazás jelszavához", + "automap": "Próbálja automatikusan feltérképezni a mappákat (\"Elküldött elemek\", \"Elküldött\" => \"Elküldött\" stb.)", + "bcc_dest_format": "A BCC-célpontnak egyetlen érvényes e-mail címnek kell lennie.
    Ha több címre kell másolatot küldenie, hozzon létre egy aliast, és használja azt itt.", + "comment_info": "A privát megjegyzés nem látható a felhasználó számára, míg a nyilvános megjegyzés tooltip-ként jelenik meg, amikor a felhasználó áttekintésében a megjegyzésre mutat.", + "created_on": "Létrehozva", + "custom_attributes": "Egyéni attribútumok", + "delete1": "Törlés a forrásból, ha befejeződött", + "delete2": "Üzenetek törlése a célállomáson, amelyek nincsenek a forráson", + "delete2duplicates": "Duplikáltak törlése a célállomáson", + "delete_ays": "Erősítse meg a törlési folyamatot.", + "disable_login": "Bejelentkezés letiltása (a bejövő leveleket továbbra is elfogadja)", + "domain": "Domain szerkesztése", + "domain_admin": "Domain adminisztrátor szerkesztése", + "domain_footer": "Tartományszintű lábléc", + "domain_footer_html": "HTML lábléc", + "domain_footer_info": "A tartományszintű lábléceket a tartományon belüli címmel társított összes kimenő e-mailhez hozzáadják.
    A következő változók használhatók a lábléchez:", + "domain_footer_info_vars": { + "auth_user": "{= auth_user =} - Hitelesített felhasználónév az MTA által megadott", + "from_user": "{= from_user =} - A boríték feladójának felhasználói része, pl. a \"moo@mailcow.tld\" esetén \"moo\"-t ad vissza", + "from_name": "{= from_name =} - A boríték feladójának neve, pl. a \"Mailcow <moo@mailcow.tld>\" esetén \"Mailcow\"-t ad vissza", + "from_addr": "{= from_addr =} - A boríték feladójának címe", + "from_domain": "{= from_domain =} - A boríték feladójának tartománya", + "custom": "{= foo =} - Ha a postafiók rendelkezik a \"foo\" egyéni attribútummal, amelynek értéke \"bar\", akkor \"bar\"-t ad vissza" + }, + "domain_footer_plain": "Egyszerű szöveges lábléc", + "domain_footer_skip_replies": "Lábléc figyelmen kívül hagyása válasz e-maileknél", + "dont_check_sender_acl": "Küldő ellenőrzés letiltása a(z) %s tartományhoz (+ alias tartományok)", + "encryption": "Titkosítás", + "exclude": "Objektumok kizárása (regex)", + "extended_sender_acl": "Külső feladói címek", + "extended_sender_acl_info": "Importálni kell egy DKIM tartomány kulcsot, ha elérhető.
    \r\n Ne felejtse el hozzáadni ezt a szervert a megfelelő SPF TXT rekordhoz.
    \r\n Amikor egy tartományt vagy alias tartományt ad hozzá ehhez a szerverhez, amely átfedésben van egy külső címmel, a külső cím eltávolításra kerül.
    \r\n Használja a @domain.tld címet, hogy engedélyezze a küldést *@domain.tld néven.", + "force_pw_update": "Jelszófrissítés kényszerítése a következő bejelentkezéskor", + "force_pw_update_info": "Ez a felhasználó csak a(z) %s címre tud bejelentkezni. Az alkalmazás jelszavak használhatóak maradnak.", + "footer_exclude": "Kizárás a láblécből", + "full_name": "Teljes név", + "gal": "Globális címlista", + "gal_info": "A GAL tartalmazza a tartomány összes objektumát, és egyetlen felhasználó sem szerkesztheti. A Szabad/Foglalt információ a SOGo-ban hiányzik, ha le van tiltva! Indítsa újra a SOGo-t a változások alkalmazásához.", + "generate": "generál", + "grant_types": "Engedélyezési típusok", + "hostname": "Hosztnév", + "inactive": "Inaktív", + "kind": "Típus", + "last_modified": "Utoljára módosítva", + "lookup_mx": "A cél egy reguláris kifejezés, amely illeszkedik az MX névre (.*\\.google\\.com, hogy a google.com-ra végződő MX-en keresztül továbbítsa az összes levelet)", + "mailbox": "Postafiók szerkesztése", + "mailbox_quota_def": "Alapértelmezett postafiók kvóta", + "mailbox_relayhost_info": "A postafiókra és csak a közvetlen aliasokra vonatkozik, felülírja a tartományi továbbító hostot.", + "mailbox_rename": "Postafiók átnevezése", + "mailbox_rename_agree": "Készítettem biztonsági másolatot.", + "mailbox_rename_warning": "FONTOS! Hozzon létre biztonsági másolatot a postafiók átnevezése előtt.", + "mailbox_rename_alias": "Alias automatikus létrehozása", + "mailbox_rename_title": "Új helyi postafiók neve", + "max_aliases": "Max. aliasok", + "max_mailboxes": "Max. lehetséges postafiókok", + "max_quota": "Max. kvóta postafiókonként (MiB)", + "maxage": "Üzenetek maximális életkora napokban, amelyek lekérdezésre kerülnek a távolról
    (0 = figyelmen kívül hagyás)", + "maxbytespersecond": "Max. bájt/másodperc
    (0 = korlátlan)", + "mbox_rl_info": "Ez a sebességkorlátozás a SASL bejelentkezési névre vonatkozik, és illeszkedik minden, a bejelentkezett felhasználó által használt \"from\" címre. Egy postafiók sebességkorlátozása felülírja a tartományszintű sebességkorlátozást.", + "mins_interval": "Időköz (perc)", + "multiple_bookings": "Több foglalás", + "none_inherit": "Nincs / Örökölt", + "nexthop": "Következő ugrás", + "password": "Jelszó", + "password_recovery_email": "Jelszó-helyreállítási e-mail", + "password_repeat": "Megerősítő jelszó (ismétlés)", + "previous": "Előző oldal", + "private_comment": "Privát megjegyzés", + "public_comment": "Nyilvános megjegyzés", + "pushover": "Pushover", + "pushover_evaluate_x_prio": "Magas prioritású levelek továbbítása [X-Priority: 1]", + "pushover_info": "A push értesítési beállítások az összes tiszta (nem-spam) levélre vonatkoznak, amelyeket a %s címre kézbesítettek, beleértve az aliasokat is (megosztott, nem megosztott, címkézett).", + "pushover_only_x_prio": "Csak a magas prioritású leveleket vegye figyelembe [X-Priority: 1]", + "pushover_sender_array": "Csak a következő feladói e-mail címeket vegye figyelembe (vesszővel elválasztva)", + "pushover_sender_regex": "Feladók egyeztetése a következő regex-szel", + "pushover_text": "Értesítési szöveg", + "pushover_title": "Értesítési cím", + "pushover_sound": "Hang", + "pushover_vars": "Ha nincs feladói szűrő meghatározva, minden e-mail figyelembe vételre kerül.
    A Regex szűrők, valamint a pontos feladói ellenőrzések egyénileg definiálhatók, és sorban lesznek figyelembe véve. Nem függenek egymástól.
    Használható változók a szöveghez és a címhez (kérjük, vegye figyelembe az adatvédelmi szabályzatokat)", + "pushover_verify": "Hitelesítő adatok ellenőrzése", + "quota_mb": "Kvóta (MiB)", + "quota_warning_bcc": "Kvóta figyelmeztetés BCC", + "quota_warning_bcc_info": "A figyelmeztetések külön másolatként lesznek elküldve a következő címzetteknek. A tárgyat a megfelelő felhasználónévvel egészítik ki zárójelben, például: Quota warning (user@example.com).", + "ratelimit": "Arányszám-korlát", + "redirect_uri": "Átirányítási/Visszahívási URL", + "relay_all": "Az összes címzett továbbítása", + "relay_all_info": "↪ Ha úgy döntesz, hogy nem továbbítod az összes címzettet, akkor minden egyes címzett számára, akit továbbítani kell, létre kell hoznod egy (\"vak\") postafiókot.", + "relay_domain": "Továbbítsa ezt a tartományt", + "relay_transport_info": "
    Információ
    Definiálhatsz szállítási térképeket ehhez a tartományhoz egyedi célállomásra. Ha nincs beállítva, egy MX lekérdezés történik.", + "relay_unknown_only": "Csak a nem létező postafiókok továbbítása. A létező postafiókok helyben lesznek kézbesítve.", + "relayhost": "Feladófüggő szállítások", + "remove": "Eltávolítás", + "resource": "Erőforrás", + "save": "Módosítások mentése", + "scope": "Hatókör", + "sender_acl": "Engedélyezés küldésre mint", + "sender_acl_disabled": "A küldő ellenőrzése le van tiltva", + "sender_acl_info": "Ha az A postafiók felhasználója küldhet a B postafiók felhasználójaként, a feladói cím nem jelenik meg automatikusan választható \"from\" mezőként a SOGo-ban.
    \r\n A B postafiók felhasználójának delegálást kell létrehoznia a SOGo-ban, hogy az A postafiók felhasználója kiválaszthassa a címét feladóként. A SOGo-ban egy postafiók delegálásához használja a menüt (három pont) a postafiók neve jobb oldalán a bal felső sarokban, a levélnézetben. Ez a viselkedés nem vonatkozik az alias címekre.", + "sieve_desc": "Rövid leírás", + "sieve_type": "Szűrő típusa", + "skipcrossduplicates": "Átugrani a duplikált üzeneteket a mappák között (aki előbb jön, előbb kapja)", + "sogo_access": "Közvetlen továbbítás a SOGo-ra", + "sogo_access_info": "A bejelentkezés után a felhasználó automatikusan átirányításra kerül a SOGo-ra.", + "sogo_visible": "Alias látható a SOGo-ban", + "sogo_visible_info": "Ez az opció csak azokra az objektumokra vonatkozik, amelyek megjeleníthetők a SOGo-ban (megosztott vagy nem megosztott alias címek, amelyek legalább egy helyi postafiókra mutatnak). Ha el van rejtve, egy alias nem jelenik meg választható feladóként a SOGo-ban.", + "spam_alias": "Időkorlátos alias címek létrehozása vagy módosítása", + "spam_filter": "Spam szűrő", + "spam_policy": "Elemek hozzáadása vagy eltávolítása a tiltó- vagy engedélyezési listán", + "spam_score": "Egyéni spam pontszám beállítása", + "subfolder2": "Szinkronizálás a célállomáson egy almappába
    (üres = ne használjon almappát)", + "syncjob": "Szinkronizálási feladat szerkesztése", + "target_address": "Célcím(ek) (vesszővel elválasztva)", + "target_domain": "Cél domain", + "timeout1": "Időtúllépés a távoli állomáshoz való csatlakozáskor", + "timeout2": "Időtúllépés a helyi állomáshoz való csatlakozáskor", + "title": "Objektum szerkesztése", + "unchanged_if_empty": "Ha nem változik, hagyja üresen", + "username": "Felhasználónév", + "validate_save": "Érvényesítés és mentés" }, "footer": { "cancel": "Mégse", @@ -159,7 +445,9 @@ "restart_container": "Konténer újraindítása", "restart_container_info": "Fontos: A teljes újraindulás eltarthat egy ideig, kérjük várjon, amíg befejeződik!", "restart_now": "Újraindítás most", - "restarting_container": "Konténer újraindítása. Ez eltarthat egy darabig." + "restarting_container": "Konténer újraindítása. Ez eltarthat egy darabig.", + "hibp_check": "Ellenőrzés a haveibeenpwned.com-on", + "nothing_selected": "Nincs semmi kiválasztva" }, "header": { "administration": "Beállítások és részletek", @@ -170,7 +458,8 @@ "quarantine": "Karantén", "restart_netfilter": "Netfilter újraindítása", "restart_sogo": "SOGo újraindítása", - "user_settings": "Felhasználó beállításai" + "user_settings": "Felhasználó beállításai", + "mailcow_system": "Rendszer" }, "info": { "awaiting_tfa_confirmation": "TFA megerősítésre várakozás", @@ -182,7 +471,23 @@ "login": "Bejelentkezés", "mobileconfig_info": "A kért Apple kapcsolat profil letöltéséhez jelentkezzen be, mint postafiók-felhasználó.", "password": "Jelszó", - "username": "Felhasználónév" + "username": "Felhasználónév", + "back_to_mailcow": "Vissza a mailcow-hoz", + "fido2_webauthn": "FIDO2/WebAuthn bejelentkezés", + "forgot_password": "> Elfelejtett jelszó?", + "invalid_pass_reset_token": "A jelszó-helyreállítási token érvénytelen vagy lejárt.
    Kérjen új jelszó-helyreállítási linket.", + "login_linkstext": "Nem a megfelelő bejelentkezés?", + "login_usertext": "Bejelentkezés felhasználóként", + "login_domainadmintext": "Bejelentkezés domain adminisztrátorként", + "login_admintext": "Bejelentkezés adminisztrátorként", + "login_user": "Felhasználói bejelentkezés", + "login_dadmin": "Domain-adminisztrátori bejelentkezés", + "login_admin": "Adminisztrátori bejelentkezés", + "new_password": "Új jelszó", + "new_password_confirm": "Új jelszó megerősítése", + "other_logins": "vagy bejelentkezés", + "reset_password": "Jelszó visszaállítása", + "request_reset_password": "Jelszó módosítás kérése" }, "mailbox": { "action": "Művelet", @@ -200,10 +505,10 @@ "add_resource": "Erőforrás hozzáadása", "add_tls_policy_map": "TLS irányelv-térkép hozzáadása", "address_rewriting": "Címátírás", - "alias": "Alias", + "alias": "Álnév", "alias_domain_backupmx": "Alias domain inaktív a továbbító domain részére", - "aliases": "Alias-ok", - "allow_from_smtp": "Kizárólag ezek az IP-címek használhatják az SMTP-t", + "aliases": "Álnevek", + "allow_from_smtp": "Kizárólag ezek az IP címek használhatják az SMTP-t", "allow_from_smtp_info": "Hagyja üresen minden feladó engedélyezéséhez.
    IPv4/IPv6 címek és hálózatok.", "allowed_protocols": "Engedélyezett protokollok", "backup_mx": "Továbbító domain", @@ -221,7 +526,7 @@ "domain_admins": "Domain adminisztrátorok", "domain_aliases": "Domain alias-ok", "domain_quota": "Kvóta", - "domains": "Domain-ek", + "domains": "Domainek", "edit": "Szerkesztés", "empty": "Nincs találat", "enable_x": "Engedélyezés", @@ -277,7 +582,87 @@ "toggle_all": "Összes átváltása", "username": "Felhasználónév", "waiting": "Várakozás", - "weekly": "Hetente" + "weekly": "Hetente", + "add_alias_expand": "Alias kiterjesztése alias tartományokra", + "alias_domain_alias_hint": "Az aliasok nem vonatkoznak automatikusan a domain aliasokra. Egy my-alias@domain alias cím nem fedezi a my-alias@alias-domain címet (ahol az \"alias-domain\" a \"domain\" képzeletbeli alias tartománya).
    Kérjük, használjon sieve szűrőt a levél külső postafiókba történő átirányításához (lásd a \"Szűrők\" fület vagy a SOGo -> Továbbító). Használja az \"Alias kiterjesztése alias tartományokra\" opciót a hiányzó aliasok automatikus hozzáadásához.", + "all_domains": "Minden tartomány", + "bcc_info": "A BCC térképek arra szolgálnak, hogy csendesen továbbítsák az összes üzenet másolatát egy másik címre. A címzett térkép típust akkor használják, ha a helyi célpont egy levél címzettjeként működik. A feladói térképek ugyanazon elv szerint működnek.
    \r\n A helyi célpontot nem értesítik a sikertelen kézbesítésről.", + "bcc_map_type": "BCC típus", + "bcc_maps": "BCC térképek", + "bcc_rcpt_map": "Címzett térkép", + "bcc_sender_map": "Feladó térkép", + "bcc_to_rcpt": "Váltás címzett térkép típusra", + "bcc_to_sender": "Váltás feladó térkép típusra", + "bcc_type": "BCC típus", + "booking_null": "Mindig szabadként mutat", + "booking_0_short": "Mindig szabad", + "booking_custom": "Kemény korlát a foglalások egyéni számára", + "booking_custom_short": "Kemény korlát", + "booking_ltnull": "Korlátlan, de foglaltnak mutatja magát, ha le van foglalva", + "booking_lt0_short": "Lágy korlát", + "catch_all": "Catch-All", + "created_on": "Létrehozva", + "dkim_domains_selector": "Válogató", + "dkim_key_length": "DKIM kulcs hossza (bit)", + "domain_templates": "Domain sablonok", + "domain_quota_total": "Teljes domain kvóta", + "gal": "Globális címlista", + "goto_ham": "Tanulás ham-ként", + "goto_spam": "Tanulás spam-ként", + "iam": "Azonosítási szolgáltató", + "last_modified": "Utoljára módosítva", + "last_pw_change": "Utolsó jelszócsere", + "mailbox_defaults": "Alapértelmezett beállítások", + "mailbox_defaults_info": "Alapértelmezett beállítások meghatározása az új postafiókokhoz.", + "mailbox_templates": "Postafiók sablonok", + "max_aliases": "Max. aliasok", + "max_mailboxes": "Max. lehetséges postafiókok", + "max_quota": "Max. kvóta postafiókonként", + "no": "✕", + "open_logs": "Naplók megnyitása", + "q_add_header": "amikor a levélszemét mappába kerül", + "q_all": " amikor a levélszemét mappába kerül és elutasításkor", + "q_reject": "elutasításkor", + "quarantine_category": "Karantén értesítési kategória", + "recipient": "Címzett", + "recipient_map_info": "A címzett térképeket arra használják, hogy egy üzenet célcímét kicseréljék, mielőtt azt kézbesítenék.", + "recipient_map_new_info": "A címzett térkép célja érvényes e-mail címnek vagy tartománynévnek kell lennie.", + "recipient_map_old_info": "Egy címzett térkép eredeti céljának érvényes e-mail címnek vagy tartománynévnek kell lennie.", + "relay_all": "Az összes címzett továbbítása", + "relay_unknown": "Ismeretlen postafiókok továbbítása", + "sender": "Feladó", + "set_postfilter": "Megjelölés poszt-szűrőként", + "set_prefilter": "Megjelölés pre-szűrőként", + "sieve_info": "Több szűrőt is tárolhatsz felhasználónként, de egyszerre csak egy pre-szűrő és egy poszt-szűrő lehet aktív.
    \r\nMinden szűrő a leírt sorrendben lesz feldolgozva. Sem a sikertelen szkript, sem a kiadott \"keep;\" parancs nem állítja le a további szkriptek feldolgozását. A globális sieve szkriptek változásai a Dovecot újraindítását váltják ki.

    Globális sieve pre-szűrő • Pre-szűrő • Felhasználói szkriptek • Poszt-szűrő • Globális sieve poszt-szűrő", + "sieve_preset_1": "Levél eldobása valószínűleg veszélyes fájltípusokkal", + "sieve_preset_2": "Egy adott feladó e-mailjét mindig olvasottként jelölje meg", + "sieve_preset_3": "Csendesen elvetni, leállítani minden további sieve feldolgozást", + "sieve_preset_4": "Fájl az INBOX-ba, kihagyni a további feldolgozást a sieve szűrőkkel", + "sieve_preset_5": "Auto válaszadó (szabadság)", + "sieve_preset_6": "Levél elutasítása válasszal", + "sieve_preset_7": "Átirányítás és megtartás/eldobás", + "sieve_preset_8": "E-mail átirányítása egy adott feladótól, olvasottként jelölés és almappába rendezés", + "sieve_preset_header": "Kérjük, nézze meg az alábbi példa-előrebeállításokat. További részletekért lásd a Wikipedia-t.", + "sogo_visible": "Alias látható a SOGo-ban", + "sogo_visible_n": "Alias elrejtése a SOGo-ban", + "sogo_visible_y": "Alias megjelenítése a SOGo-ban", + "syncjob_check_log": "Napló ellenőrzése", + "syncjob_last_run_result": "Utolsó futás eredménye", + "syncjob_EX_OK": "Siker", + "syncjob_EXIT_CONNECTION_FAILURE": "Kapcsolati probléma", + "syncjob_EXIT_TLS_FAILURE": "Probléma a titkosított kapcsolattal", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Hitelesítési probléma", + "syncjob_EXIT_OVERQUOTA": "A cél postafiók túllépte a kvótát", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nem lehet csatlakozni a távoli szerverhez", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Hibás felhasználónév vagy jelszó", + "templates": "Sablonok", + "template": "Sablon", + "tls_map_policy": "Irányelv", + "tls_policy_maps": "TLS irányelv térképek", + "tls_policy_maps_enforced_tls": "Ezek az irányelvek felülírják a kimenő TLS szállítási szabályokat függetlenül a felhasználó TLS irányelvi beállításaitól. Ha nincs alább egyetlen irányelv sem, ezek a felhasználók az alapértelmezett értékeket alkalmazzák, amelyeket a smtp_tls_mandatory_protocols és a smtp_tls_mandatory_ciphers határoz meg.", + "tls_policy_maps_info": "Ez az irányelv térkép felülírja a kimenő TLS szállítási szabályokat függetlenül a felhasználó TLS irányelvi beállításaitól.
    \r\n Kérjük, ellenőrizze a \"smtp_tls_policy_maps\" dokumentációját további információkért.", + "tls_policy_maps_long": "Kimenő TLS irányelv térkép felülírások", + "yes": "✓" }, "oauth2": { "access_denied": "Kérjük jelentkezzen be postafiók felhasználójával az OAuth2 használatához.", @@ -301,7 +686,7 @@ "medium_danger": "Közepes veszély", "neutral_danger": "Semleges", "notified": "Értesítve", - "qinfo": "A karantén-rendszer elmenti a visszautasított leveleket az adatbázisba, miközben a feladónak nem lesz az a benyomása, hogy a levelet kézbesítették.\r\n
    \"Spamnek jelölés és törlés\" spam-ként jegyzi meg az üzenetet a Bayes-tétel segítségével, illetve fuzzy hash-eket számít, hogy hasonló üzenetek is spam-be kerüljenek a jövőben.\r\n
    Több üzenet spamként való megjegyzése időigényes lehet.
    Feketelistára vett levelek nem kerülnek karanténba.", + "qinfo": "A karantén-rendszer elmenti a visszautasított leveleket az adatbázisba, miközben a feladónak nem lesz az a benyomása, hogy a levelet kézbesítették.\r\n
    \"Spamnek jelölés és törlés\" spam-ként jegyzi meg az üzenetet a Bayes-tétel segítségével, illetve fuzzy hash-eket számít, hogy hasonló üzenetek is spam-be kerüljenek a jövőben.\r\n
    Több üzenet spamként való megjegyzése időigényes lehet.
    Tiltólistára vett levelek nem kerülnek karanténba.", "qitem": "Tétel", "quarantine": "Karantén", "quick_actions": "Műveletek", @@ -324,10 +709,38 @@ "table_size_show_n": "%s tétel mutatása", "text_from_html_content": "Tartalom (konvertált html)", "text_plain_content": "Tartalom (sima szöveg)", - "toggle_all": "Összes átkapcsolása" + "toggle_all": "Összes átkapcsolása", + "check_hash": "Fájl hash keresése @ VT", + "confirm": "Megerősítés", + "deliver_inbox": "Kézbesítés a beérkező mappába", + "disabled_by_config": "A jelenlegi rendszerkonfiguráció letiltja a karantén funkcionalitást. Kérjük, állítsa be a \"megőrzéseket postafiókonként\" és a \"maximális méretet\" a karantén elemek számára.", + "info": "Információ", + "junk_folder": "Levélszemét mappa", + "qhandler_success": "A kérés sikeresen elküldve a rendszernek. Most már bezárhatja az ablakot.", + "qid": "Rspamd QID", + "rejected": "Elutasítva", + "release_body": "A levelét eml fájlként csatoltuk ehhez az üzenethez.", + "rewrite_subject": "Tárgy átírása", + "settings_info": "A karanténba helyezhető elemek maximális száma: %s
    Maximális e-mail méret: %s MiB", + "spam": "Spam", + "quick_info_link": "Információs link megnyitása", + "type": "Típus" }, "queue": { - "queue_manager": "Queue Manager" + "queue_manager": "Queue Manager", + "delete": "Összes törlése", + "flush": "Üzenetsor ürítése", + "info": "A levelezési üzenetsor tartalmazza az összes kézbesítésre váró e-mailt. Ha egy e-mail hosszú ideig ragad az üzenetsorban, a rendszer automatikusan törli.
    A megfelelő levél hibaüzenete tájékoztatást ad arról, miért nem lehetett a levelet kézbesíteni.", + "legend": "A levelezési üzenetsor műveleti funkciói:", + "ays": "Kérjük, erősítse meg, hogy törölni szeretné az összes elemet az aktuális üzenetsorból.", + "deliver_mail": "Kézbesítés", + "deliver_mail_legend": "Megpróbálja újra kézbesíteni a kiválasztott leveleket.", + "hold_mail": "Visszatartás", + "hold_mail_legend": "Visszatartja a kiválasztott leveleket. (Megakadályozza a további kézbesítési kísérleteket)", + "show_message": "Üzenet megjelenítése", + "unban": "üzenetsor tiltás feloldása", + "unhold_mail": "Visszatartás feloldása", + "unhold_mail_legend": "Kiadja a kiválasztott leveleket a kézbesítéshez. (Előzetes visszatartás szükséges)" }, "start": { "help": "Súgó panel megjelenítése/elrejtése", @@ -380,7 +793,49 @@ "resource_modified": "Postafiók %s módosításai mentve", "resource_removed": "Erőforrás %s eltávolítva", "saved_settings": "Beállítások mentve", - "upload_success": "File sikeresen feltöltve" + "upload_success": "File sikeresen feltöltve", + "acl_saved": "Az ACL a(z) %s objektumhoz mentve", + "bcc_deleted": "BCC térkép bejegyzések törölve: %s", + "bcc_edited": "BCC térkép bejegyzés %s szerkesztve", + "bcc_saved": "BCC térkép bejegyzés mentve", + "cors_headers_edited": "A CORS beállítások mentve", + "custom_login_modified": "A bejelentkezési testreszabás sikeresen mentve", + "domain_add_dkim_available": "Egy DKIM kulcs már létezett", + "dkim_duplicated": "A DKIM kulcs a(z) %s tartományhoz sikeresen átmásolva a(z) %s tartományba", + "domain_footer_modified": "A(z) %s tartományi lábléc módosításai mentve", + "f2b_banlist_refreshed": "A tiltólista azonosítója sikeresen frissítve.", + "f2b_modified": "A Fail2ban paraméterek módosításai mentve", + "forwarding_host_added": "A továbbító állomás %s hozzáadva", + "forwarding_host_removed": "A továbbító állomás %s eltávolítva", + "iam_test_connection": "Kapcsolat sikeres", + "ip_check_opt_in_modified": "Az IP ellenőrzés sikeresen mentve", + "nginx_reloaded": "Az Nginx újra lett töltve", + "password_policy_saved": "A jelszó házirend sikeresen mentve", + "password_changed_success": "A jelszó sikeresen megváltoztatva", + "pushover_settings_edited": "A Pushover beállítások sikeresen elmentve, kérjük, ellenőrizze a hitelesítő adatokat.", + "queue_command_success": "A sor parancs sikeresen befejeződött", + "recipient_map_entry_deleted": "A címzett térkép ID %s törölve", + "recipient_map_entry_saved": "A címzett térkép bejegyzés \"%s\" mentve", + "recovery_email_sent": "Helyreállítási e-mail elküldve a(z) %s címre", + "relayhost_added": "A térkép bejegyzés %s hozzáadva", + "relayhost_removed": "A térkép bejegyzés %s eltávolítva", + "reset_main_logo": "Visszaállítás alapértelmezett logóra", + "rl_saved": "Arányszám-korlát a(z) %s objektumhoz mentve", + "rspamd_ui_pw_set": "Rspamd UI jelszó sikeresen beállítva", + "settings_map_added": "Hozzáadott beállítási térkép bejegyzés", + "settings_map_removed": "Eltávolított beállítási térkép ID %s", + "sogo_profile_reset": "A SOGo profil a(z) %s felhasználóhoz visszaállítva", + "template_added": "Hozzáadott sablon %s", + "template_modified": "A(z) %s sablon módosításai mentve", + "template_removed": "A sablon ID %s törölve", + "tls_policy_map_entry_deleted": "TLS irányelv térkép ID %s törölve", + "tls_policy_map_entry_saved": "TLS irányelv térkép bejegyzés \"%s\" mentve", + "ui_texts": "A felhasználói felület szövegeinek módosításai mentve", + "verified_fido2_login": "FIDO2 bejelentkezés ellenőrizve", + "verified_totp_login": "TOTP bejelentkezés ellenőrizve", + "verified_webauthn_login": "WebAuthn bejelentkezés ellenőrizve", + "verified_yotp_login": "Yubico OTP bejelentkezés ellenőrizve", + "mailbox_renamed": "A postafiók átnevezve %s-ről %s-re" }, "user": { "action": "Művelet", @@ -449,15 +904,14 @@ "spam_score_reset": "Szerver szerinti alapértelmezésre visszaállítás", "spamfilter": "Spam szűrő", "spamfilter_behavior": "Osztályozás", - "spamfilter_bl": "Feketelista", - "spamfilter_bl_desc": "A feketelistán szereplő email címek mindig spam-ként lesznek kezelve és vissza lesznek utasítva. Joker karakter használható. A szűrő csak közvetlen aliasokra vonatkozik (alias egyetlen cél-postafiókkal), magára a postafiókra, illetve mindent elkapó aliasokra nem.", + "spamfilter_bl": "Tiltólista", + "spamfilter_bl_desc": "A tiltólistán szereplő email címek mindig spam-ként lesznek kezelve és vissza lesznek utasítva. Joker karakter használható. A szűrő csak közvetlen aliasokra vonatkozik (alias egyetlen cél-postafiókkal), magára a postafiókra, illetve mindent elkapó aliasokra nem.", "spamfilter_default_score": "Alapértelmezett értékek", "spamfilter_green": "Zöld: ez az üzenet nem spam", "spamfilter_hint": "Az első érték az alacsony spam pontszámot, a második a magas spam pontszámot jelöli.", "spamfilter_red": "Vörös: Ez az üzenet spam, a szerver el fogja vetni.", "spamfilter_table_action": "Művelet", "spamfilter_table_add": "Tétel hozzáadása", - "spamfilter_table_empty": "Nincs megjeleníthető adat", "spamfilter_table_remove": "eltávolítás", "spamfilter_table_rule": "Szabály", "spamfilter_wl": "Engedélyezőlista", @@ -482,15 +936,88 @@ "waiting": "Várakozás", "week": "hét", "weekly": "heti", - "weeks": "hét" + "weeks": "hét", + "aliases_also_send_as": "Küldhet még mint felhasználó", + "aliases_send_as_all": "Ne ellenőrizze a küldő hozzáférését a következő tartomány(ok) és annak alias tartományai számára", + "app_hint": "Az alkalmazás jelszavak alternatív jelszavak az IMAP, SMTP, CalDAV, CardDAV és EAS bejelentkezéshez. A felhasználónév változatlan marad. A SOGo webmail nem érhető el az alkalmazás jelszavakon keresztül.", + "allowed_protocols": "Engedélyezett protokollok", + "apple_connection_profile_complete": "Ez a kapcsolati profil tartalmazza az IMAP és SMTP paramétereket, valamint a CalDAV (naptárak) és CardDAV (névjegyek) útvonalakat egy Apple eszközhöz.", + "apple_connection_profile_mailonly": "Ez a kapcsolati profil csak IMAP és SMTP konfigurációs paramétereket tartalmaz egy Apple eszközhöz.", + "apple_connection_profile_with_app_password": "Új alkalmazás jelszó generálódik és hozzáadódik a profilhoz, így nem kell jelszót megadni az eszköz beállításakor. Kérjük, ne ossza meg a fájlt, mivel teljes hozzáférést biztosít a postafiókjához.", + "attribute": "Attribútum", + "authentication": "Hitelesítés", + "change_password_hint_app_passwords": "Fiókodnak %d alkalmazás jelszava van, amelyek nem lesznek megváltoztatva. Ezek kezeléséhez menj az Alkalmazás jelszavak fülre.", + "clear_recent_successful_connections": "Sikeres kapcsolatok törlése", + "created_on": "Létrehozva", + "direct_aliases_desc": "A közvetlen alias címeket érintik a spam szűrő és a TLS irányelv beállításai.", + "direct_protocol_access": "Ennek a postafiók felhasználónak közvetlen, külső hozzáférése van a következő protokollokhoz és alkalmazásokhoz. Ezt a beállítást az adminisztrátor vezérli. Az alkalmazás jelszavak létrehozhatók, hogy hozzáférést biztosítsanak az egyedi protokollokhoz és alkalmazásokhoz.
    A \"Webmail\" gomb egységes bejelentkezést biztosít a SOGo-ra, és mindig elérhető.", + "eas_reset_help": "Sok esetben a eszköz gyorsítótárának visszaállítása segít egy sérült ActiveSync profil helyreállításában.
    Figyelem: Minden elem újra letöltődik!", + "empty": "Nincs eredmény", + "from": "feladó", + "is_catch_all": "Catch-all a tartomány(ok)hoz", + "last_pw_change": "Utolsó jelszócsere", + "last_ui_login": "Utolsó UI bejelentkezés", + "login_history": "Bejelentkezési előzmények", + "mailbox": "Postafiók", + "mailbox_general": "Általános", + "mailbox_settings": "Beállítások", + "month": "hónap", + "months": "hónap", + "open_logs": "Naplók megnyitása", + "open_webmail_sso": "Webmail", + "overview": "Áttekintés", + "password_reset_info": "Ha nincs megadva e-mail a jelszó-helyreállításhoz, ez a funkció nem használható.", + "protocols": "Protokollok", + "pushover_evaluate_x_prio": "Magas prioritású levelek továbbítása [X-Priority: 1]", + "pushover_info": "A push értesítési beállítások az összes tiszta (nem-spam) levélre vonatkoznak, amelyeket a %s címre kézbesítettek, beleértve az aliasokat is (megosztott, nem megosztott, címkézett).", + "pushover_only_x_prio": "Csak a magas prioritású leveleket vegye figyelembe [X-Priority: 1]", + "pushover_sender_array": "A következő feladói e-mail címek figyelembe vétele (vesszővel elválasztva)", + "pushover_sender_regex": "Feladók egyeztetése a következő regex-szel", + "pushover_text": "Értesítési szöveg", + "pushover_title": "Értesítési cím", + "pushover_sound": "Hang", + "pushover_vars": "Ha nincs feladói szűrő meghatározva, minden e-mail figyelembe vételre kerül.
    A Regex szűrők, valamint a pontos feladói ellenőrzések egyénileg definiálhatók, és sorban lesznek figyelembe véve. Nem függenek egymástól.
    Használható változók a szöveghez és a címhez (kérjük, vegye figyelembe az adatvédelmi szabályzatokat)", + "pushover_verify": "Hitelesítő adatok ellenőrzése", + "pw_recovery_email": "Jelszó-helyreállítási e-mail", + "q_add_header": "Levélszemét mappa", + "q_all": "Összes kategória", + "q_reject": "Elutasítva", + "quarantine_category": "Karantén értesítési kategória", + "quarantine_category_info": "Az \"Elutasítva\" értesítési kategória magában foglalja azokat a leveleket, amelyeket elutasítottak, míg a \"Levélszemét mappa\" értesíti a felhasználót a levélszemét mappába helyezett levelekről.", + "recent_successful_connections": "Látott sikeres kapcsolatok", + "shared_aliases_desc": "A megosztott aliasokat nem érintik a felhasználóspecifikus beállítások, mint a spam szűrő vagy a titkosítási szabályzat. A megfelelő spam szűrőket csak egy adminisztrátor hozhatja létre tartományszintű szabályzatként.", + "sogo_profile_reset": "SOGo profil visszaállítása", + "sogo_profile_reset_help": "Ez tönkreteszi a felhasználó SOGo profilját, és visszavonhatatlanul törli az összes névjegy- és naptáradatot.", + "sogo_profile_reset_now": "Profil visszaállítása most", + "spamfilter_table_domain_policy": "n/a (tartományi szabályzat)", + "syncjob_check_log": "Napló ellenőrzése", + "syncjob_last_run_result": "Utolsó futás eredménye", + "syncjob_EX_OK": "Siker", + "syncjob_EXIT_CONNECTION_FAILURE": "Kapcsolati probléma", + "syncjob_EXIT_TLS_FAILURE": "Probléma a titkosított kapcsolattal", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Hitelesítési probléma", + "syncjob_EXIT_OVERQUOTA": "A cél postafiók túllépte a kvótát", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nem lehet csatlakozni a távoli szerverhez", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Hibás felhasználónév vagy jelszó", + "tfa_info": "A kétlépcsős hitelesítés segít megvédeni a fiókodat. Ha engedélyezed, alkalmazás jelszavakra lesz szükséged, hogy bejelentkezz olyan alkalmazásokba vagy szolgáltatásokba, amelyek nem támogatják a kétlépcsős hitelesítést (pl. levelező kliensek).", + "tls_policy_warning": "Figyelem: Ha titkosított levélátvitelt kényszerítesz ki, elveszíthetsz e-maileket.
    Azok az üzenetek, amelyek nem felelnek meg a szabályzatnak, kemény hibával visszapattannak a levelezőrendszerből.
    Ez az opció a fő e-mail címedre (bejelentkezési név), az alias tartományokból származó összes címre, valamint az alias címekre vonatkozik, amelyeknek csak ez az egy postafiók a célja.", + "value": "Érték", + "with_app_password": "alkalmazás jelszóval", + "year": "év", + "years": "év" }, "warning": { "cannot_delete_self": "Bejelentkezett felhasználó nem törölhető.", - "ip_invalid": "Érvénytelen IP-cím átugorva: %s", + "ip_invalid": "Érvénytelen IP cím átugorva: %s", "no_active_admin": "Utolsó aktív admin felhasználó nem deaktiválható.", "quota_exceeded_scope": "Domain kvóta átlépve: csak korlátok nélküli postafiókok hozhatók létre ebben a domain-ben.", - "session_token": "Űrlap-token érvénytelen: Tokenek nem egyeznek", - "session_ua": "Űrlap-token érvénytelen: User-Agent hitelesítési probléma" + "session_token": "Űrlaptoken érvénytelen: Tokenek nem egyeznek", + "session_ua": "Űrlaptoken érvénytelen: User-Agent hitelesítési probléma", + "domain_added_sogo_failed": "A tartomány hozzáadva, de a SOGo újraindítása sikertelen, kérjük, ellenőrizze a szerver naplókat.", + "dovecot_restart_failed": "A Dovecot újraindítása sikertelen, kérjük, ellenőrizze a naplókat", + "fuzzy_learn_error": "Fuzzy hash tanulási hiba: %s", + "hash_not_found": "A hash nem található vagy már törölve lett", + "is_not_primary_alias": "Nem elsődleges alias %s kihagyva" }, "acl": { "delimiter_action": "Elhatárolás", @@ -516,15 +1043,24 @@ "smtp_ip_access": "Az SMTP engedélyezett állomásainak módosítása", "sogo_profile_reset": "SOGo profil visszaállítása", "spam_alias": "Ideiglenes álnevek", - "spam_policy": "Fekete/Fehér lista", + "spam_policy": "Tiltó/engedélyezési lista", "spam_score": "Spam pontszám", "syncjobs": "Szinkronizálási feladatok", "tls_policy": "TLS szabályzat", "unlimited_quota": "Korlátlan kvóta a postafiókok számára", - "sogo_access": "A SOGo-hozzáférés kezelésének lehetővé tétele" + "sogo_access": "A SOGo-hozzáférés kezelésének lehetővé tétele", + "pw_reset": "Lehetővé teszi a mailcow felhasználói jelszavak visszaállítását" }, "diagnostics": { - "dns_records": "DNS bejegyzések" + "dns_records": "DNS bejegyzések", + "cname_from_a": "Az A/AAAA rekordból származó érték. Ez addig támogatott, amíg a rekord a megfelelő erőforrásra mutat.", + "dns_records_24hours": "Kérjük, vegye figyelembe, hogy a DNS-ben végrehajtott változtatások akár 24 órát is igénybe vehetnek, amíg a jelenlegi állapotuk helyesen megjelenik ezen az oldalon. Ez a célja, hogy könnyen láthassa, hogyan kell konfigurálnia a DNS rekordokat, és ellenőrizze, hogy az összes rekordja helyesen van-e tárolva a DNS-ben.", + "dns_records_data": "Helyes adatok", + "dns_records_docs": "Kérjük, konzultáljon a dokumentációval is.", + "dns_records_name": "Név", + "dns_records_status": "Jelenlegi állapot", + "dns_records_type": "Típus", + "optional": "Ez a rekord opcionális." }, "add": { "username": "Felhasználónév", @@ -588,6 +1124,22 @@ "alias_domain": "Alias domain", "alias_domain_info": "Csak érvényes tartománynevek (vesszővel elválasztva).", "app_name": "Alkalmazás neve", - "app_passwd_protocols": "Engedélyezett protokollok az alkalmazás jelszavához" + "app_passwd_protocols": "Engedélyezett protokollok az alkalmazás jelszavához", + "automap": "Próbálja automatikusan feltérképezni a mappákat (\"Elküldött elemek\", \"Elküldött\" => \"Elküldött\" stb.)", + "multiple_bookings": "Több foglalás", + "quota_mb": "Kvóta (MiB)", + "relay_all": "Az összes címzett továbbítása", + "relay_all_info": "↪ Ha úgy döntesz, hogy nem továbbítod az összes címzettet, akkor minden egyes címzett számára, akit továbbítani kell, létre kell hoznod egy (\"vak\") postafiókot.", + "relay_domain": "Továbbítsa ezt a tartományt", + "relay_transport_info": "
    Információ
    Definiálhatsz szállítási térképeket ehhez a tartományhoz egyedi célállomásra. Ha nincs beállítva, egy MX lekérdezés történik.", + "relay_unknown_only": "Csak a nem létező postafiókok továbbítása. A létező postafiókok helyben lesznek kézbesítve.", + "relayhost_wrapped_tls_info": "Kérjük, ne használjon TLS-be csomagolt portokat (többnyire a 465-ös porton használatosak).
    \r\nHasználjon bármilyen nem csomagolt portot és adjon ki STARTTLS-t. A TLS kikényszerítésére egy TLS irányelv hozható létre a \"TLS irányelv térképek\" alatt.", + "select": "Kérjük, válasszon...", + "select_domain": "Kérjük, először válasszon egy domaint", + "sieve_desc": "Rövid leírás", + "sieve_type": "Szűrő típusa", + "skipcrossduplicates": "Duplikált üzenetek átugrása mappák között (érkezési sorrendben)", + "subscribeall": "Feliratkozás minden mappára", + "syncjob": "Szinkronizálási feladat hozzáadása" } -} \ No newline at end of file +} diff --git a/mailcow/data/web/lang/lang.it-it.json b/mailcow/data/web/lang/lang.it-it.json index fdfa781..a2f0a87 100644 --- a/mailcow/data/web/lang/lang.it-it.json +++ b/mailcow/data/web/lang/lang.it-it.json @@ -353,7 +353,30 @@ "password_settings": "Impostazioni della password", "queue_unban": "sblocca", "restore_template": "Lasciare vuoto per ripristinare il modello predefinito.", - "logo_normal_label": "Normale" + "logo_normal_label": "Normale", + "app_hide": "Nascondi per il login", + "ip_check_disabled": "Il controllo IP è disabilitato. Puoi abilitarlo sotto
    Sistema > Configurazione > Opzioni > Personalizza ", + "ip_check_opt_in": "Attiva l'utilizzo dei servizi di terze parti ipv4.mailcow.email e ipv6.mailcow.email per risolvere gli indirizzi IP esterni", + "iam_login_provisioning": "Creazione automatica di utenti al login", + "iam_port": "Porta", + "iam_redirect_url": "URL di reindirizzamento", + "iam_server_url": "URL server", + "iam_sso": "Single Sign-On", + "iam_test_connection": "Test connessione", + "iam_use_ssl": "Usa SSL", + "iam_use_tls": "Usa StartTLS", + "iam_version": "Versione", + "ignore_ssl_error": "Ignorare gli errori SSL", + "task": "Attività", + "login_page": "Pagina di accesso", + "filter": "Filtro", + "iam_attribute_field": "Campo attributo", + "iam_authorize_url": "Endpoint di autorizzazione", + "iam_auth_flow": "Flusso di autenticazione", + "iam_client_id": "ID cliente", + "iam_default_template": "Template predefinito", + "iam_host": "Host", + "iam_import_users": "Importa utenti" }, "danger": { "access_denied": "Accesso negato o form di login non corretto", @@ -484,7 +507,11 @@ "password_reset_invalid_user": "La casella di posta elettronica non è stata trovata o non è stata impostata un'e-mail di recupero", "password_reset_na": "Il recupero della password non è attualmente disponibile. Contattare l'amministratore.", "recovery_email_failed": "Impossibile inviare un'e-mail di recupero. Contattare l'amministratore.", - "to_invalid": "Il destinatario non deve essere vuoto" + "to_invalid": "Il destinatario non deve essere vuoto", + "iam_test_connection": "Connessione non riuscita", + "webauthn_authenticator_failed": "L'autenticator selezionato non è stato trovato", + "webauthn_username_failed": "L'authenticator selezionato appartiene ad un altro account", + "webauthn_publickey_failed": "Nessuna chiave pubblica è stata salvata per l'authenticator selezionato" }, "debug": { "chart_this_server": "Grafico (questo server)", @@ -655,7 +682,10 @@ "domain_footer_html": "Piè di pagina HTML", "domain_footer_plain": "Piè di pagina PLAIN", "footer_exclude": "Escludi dal piè di pagina", - "password_recovery_email": "E-mail di recupero password" + "password_recovery_email": "E-mail di recupero password", + "mailbox_rename": "Rinominare la casella mail", + "mailbox_rename_agree": "Ho creato un backup.", + "mailbox_rename_alias": "Creare alias automaticamente" }, "fido2": { "confirm": "Conferma", @@ -717,7 +747,11 @@ "forgot_password": "> Password dimenticata?", "new_password": "Nuova password", "new_password_confirm": "Conferma la nuova password", - "reset_password": "Ripristino della password" + "reset_password": "Ripristino della password", + "login_linkstext": "Non è il login corretto?", + "login_usertext": "Accedere come utente", + "login_domainadmintext": "Accedere come domain admin", + "login_admintext": "Accedere come admin" }, "mailbox": { "action": "Azione", diff --git a/mailcow/data/web/lang/lang.ja-jp.json b/mailcow/data/web/lang/lang.ja-jp.json index 74c0424..6dfa5d8 100644 --- a/mailcow/data/web/lang/lang.ja-jp.json +++ b/mailcow/data/web/lang/lang.ja-jp.json @@ -1187,6 +1187,7 @@ "created_on": "作成日", "daily": "毎日", "day": "日", + "description": "説明", "delete_ays": "削除プロセスを確認してください。", "direct_aliases": "直接エイリアスアドレス", "direct_aliases_desc": "直接エイリアスアドレスは、スパムフィルターおよびTLSポリシー設定の影響を受けます。", @@ -1201,7 +1202,9 @@ "encryption": "暗号化", "excludes": "除外", "expire_in": "有効期限まで", + "expire_never": "有効期限なし", "fido2_webauthn": "FIDO2/WebAuthn", + "forever": "有効期限なし", "force_pw_update": "グループウェア関連サービスにアクセスするには、新しいパスワードを必ず設定する必要があります。", "from": "送信元", "generate": "生成", diff --git a/mailcow/data/web/lang/lang.lv-lv.json b/mailcow/data/web/lang/lang.lv-lv.json index 545bbe4..86eb91a 100644 --- a/mailcow/data/web/lang/lang.lv-lv.json +++ b/mailcow/data/web/lang/lang.lv-lv.json @@ -16,7 +16,16 @@ "login_as": "Pieteikšanās kā pastkastes lietotājam", "mailbox_relayhost": "Pasta kastītes relayhost maiņa", "prohibited": "Aizliegts ar ACL", - "protocol_access": "Protokola piekļuves maiņa" + "protocol_access": "Protokola piekļuves maiņa", + "pw_reset": "Ļaut atiestatīt mailcow lietotāja paroli", + "ratelimit": "Piekļuves biežuma ierobežojums", + "quarantine": "Karantīnas darbības", + "quarantine_attachments": "Karantīnas pielikumi", + "quarantine_category": "Mainīt karantīnas paziņojumu kategoriju", + "quarantine_notification": "Mainīt karantīnas paziņojumus", + "smtp_ip_access": "Mainīt SMTP atļautos saimniekdatorus", + "sogo_access": "Atļaut SOGo piekļuves pārvaldību", + "sogo_profile_reset": "Atiestatīt SOGo profilu" }, "add": { "activate_filter_warn": "Visi pārējie filtri tiks deaktivizēti, kad aktīvs ir atzīmēts.", @@ -24,22 +33,22 @@ "add": "Pievienot", "add_domain_only": "Tikai pievienot domēnu", "add_domain_restart": "Pievienot domēnu un restartēt SOGo", - "alias_address": "Aizstājaddrese/s", - "alias_address_info": "Pilna epasta addrese/s vai @piemērs.com, lai notvertu visas domēna ziņas (komatu atdalītas). tikai mailcow domēni.", + "alias_address": "Aizstājadrese/s", + "alias_address_info": "Pilna epasta adrese/s vai @example.com, lai notvertu visus domēna ziņojumus (atdalītas ar komatu). Tikai mailcow domēni.", "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", - "delete1": "Dzēst no avota, kad tas ir pabeigts", + "delete1": "Izdzēst no avota pēc pabeigšanas", "delete2": "Dzēsiet ziņojumus galamērķī, kas nav avotā", - "delete2duplicates": "Dzēst dublikātus galamērķī", + "delete2duplicates": "Izdzēst atkārtojošos vienumus galamērķī", "description": "Apraksts", "domain": "Domēns", "domain_quota_m": "Kopējā domēna kvota (MiB)", "enc_method": "Šifrēšanas metode", "exclude": "Izslēgt objektus (regex)", "full_name": "Pilns vārds", - "goto_null": "Klusām dzēst pastu", + "goto_null": "Klusām atmest pastu", "hostname": "Saimniekdators", "kind": "Veids", "mailbox_quota_m": "Maks. kvota pastkastei (MiB)", @@ -63,15 +72,18 @@ "skipcrossduplicates": "Izlaist dublētus ziņojumus pa mapēm (pirmais nāk, pirmais kalpo)", "syncjob": "Pievienot sinhronizācijas darbu", "syncjob_hint": "Ņemiet vērā, ka parole ir jāuzglabā vienkāršā tekstā!", - "target_address": "Iet uz adresēm", - "target_address_info": "Pilna epasta addrese/s (comma-separated).", + "target_address": "Mērķa adreses", + "target_address_info": "Pilna epasta adrese/s (atdalītas ar komatu).", "target_domain": "Mērķa domēns", "username": "Lietotājvārds", "validate": "Apstiprināt", - "validation_success": "Apstiprināts veiksmīgi", + "validation_success": "Sekmīgi apstiprināts", "bcc_dest_format": "BCC galamērķim ir jābūt vienai derīgai e-pasta adresei.
    Ja ir nepieciešams nosūtīt kopiju vairākām adresēm, jāizveido aizstājvārds un jāizmanto tas šeit.", "domain_matches_hostname": "Domēns %s atbilst saimniekdatora nosaukumam", - "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)" + "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)", + "app_password": "Pievienot lietotnes paroli", + "app_passwd_protocols": "Atļautie lietotnes paroles protokoli", + "goto_spam": "Apgūt kā mēstuli" }, "admin": { "access": "Pieeja", @@ -104,17 +116,17 @@ "domain": "Domēns", "domain_admins": "Domēna administratori", "edit": "Labot", - "empty": "Nav rezultātu", + "empty": "Nav iznākuma", "f2b_ban_time": "Aizlieguma laiks (s)", "f2b_max_attempts": "Maks. piegājieni", "f2b_netban_ipv4": "IPv4 apakštīkla izmērs, lai piemērotu aizliegumu uz (8-32)", "f2b_netban_ipv6": "IPv6 apakštīkla izmērs, lai piemērotu aizliegumu uz (8-128)", "f2b_parameters": "Fail2ban parametri", "f2b_retry_window": "Atkārtošanas logs (s) priekš maks. piegājiena", - "f2b_whitelist": "Baltā saraksta tīkls/hosts", + "f2b_whitelist": "Atļautie tīkli/resursdatori", "filter_table": "Filtru tabula", "forwarding_hosts": "Hostu pārsūtīšana", - "forwarding_hosts_add_hint": "Var norādīt vai nu IPv4/IPv6 addreses, tīklu ar CIDR apzīmējumu, saimniekdatoru nosaukumus (kas tiks atrisināti IP adresēs) vai arī domēna vārdus (kas tiks atrisināti IP adresēs, vaicājot SPF ierakstus, vai, ja tādu nav, MX ierakstus).", + "forwarding_hosts_add_hint": "Var norādīt vai nu IPv4/IPv6 adreses, tīklu ar CIDR apzīmējumu, saimniekdatoru nosaukumus (kas tiks atrisināti IP adresēs) vai arī domēna vārdus (kas tiks atrisināti IP adresēs, vaicājot SPF ierakstus, vai, ja tādu nav, MX ierakstus).", "forwarding_hosts_hint": "Ienākošie ziņojumi tiek bez nosacījumiem pieņemti no visiem šeit norādītajiem saimniekdatoriem. Tie tad netiek pārbaudīti pret DNSBL vai pakļauti ievietošanai pelēkajā sarakstā. No tiem saņemtās mēstules nekad netiek noraidītas, bet pēc izvēles tās var pārvietot mapē \"Nevēlams\". Visbiežāk to izmanto, lai norādītu pasta serverus, kuros ir uzstādīts nosacījums, kas pārsūta ienākošās e-pasta vēstules uz Tavu mailcow serveri.", "help_text": "Pārrakstīt palīdzības tekstu zem pieteikšanās maskas (var izmantot HTML)", "host": "Hosts", @@ -142,7 +154,7 @@ "recipients": "Adresāts", "refresh": "Atsvaidzināt", "regen_api_key": "Reģenerēt API atslēgu", - "relay_from": "\"No:\" addrese", + "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.", "remove": "Noņemt", @@ -170,7 +182,10 @@ "rspamd_com_settings": "Iestatījuma nosaukums tiks izveidots automātiski. Lūgums zemāk skatīt priekšiestatījumu piemērus. Vairāk informācijas ir Rspamd dokumentācijā", "reset_password_vars": "{{link}} Izveidotā paroles atiestatīšanas saite
    {{username}} Lietotāja, kurš pieprasīja paroles atiestatīšanu, pastkastes nosaukums
    {{username2}} Atkopšanas pastkastes nosaukums
    {{date}} Paroles atiestatīšanas pieprasījuma veikšanas datums
    {{token_lifetime}} Pilnvaras derīgums minūtēs
    {{hostname}} mailcow saimniekdatora nosaukums", "ui_header_announcement_help": "Paziņojums ir redzams visiem lietotājiem, kuri ir pieteikušies, un pieteikšanās ekrānā saskarnē.", - "login_time": "Pieteikšanās laiks" + "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" }, "danger": { "access_denied": "Piekļuve liegta, vai nepareizi dati", @@ -190,8 +205,8 @@ "goto_empty": "Aizstājādresei jāsatur vismaz viena derīga mērķa adrese", "goto_invalid": "Goto adrese nepareiza", "imagick_exception": "Kļūda: Imagick izņēmums, lasot attēlu", - "img_invalid": "Nevar apstiprināt attēla failu", - "img_tmp_missing": "Nevar apstiprināt attēla failu: pagaidu failu nav atrasts", + "img_invalid": "Nevar apstiprināt attēla datni", + "img_tmp_missing": "Nevar apstiprināt attēla datni: pagaidu datne nav atrasta", "invalid_mime_type": "Nederīgs mime tips", "is_alias": "%s jau ir zināma kā aizstājadrese", "is_alias_or_mailbox": "%s jau ir zināms kā aizstājvārds, pastkaste vai aizstājadrese, kas ir izvērsta no aizstājdomēna.", @@ -222,7 +237,11 @@ "targetd_not_found": "Mērķa domēns nav atrasts", "username_invalid": "Lietotājvārds nevar tikt izmantots", "validity_missing": "Lūdzu piešķiriet derīguma termiņu", - "domain_cannot_match_hostname": "Domēns nevar atbilst saimniekdatora nosaukumam" + "domain_cannot_match_hostname": "Domēns nevar atbilst saimniekdatora nosaukumam", + "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" }, "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.", @@ -239,9 +258,9 @@ "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", - "delete1": "Dzēst no avota, kad pabeigts", + "delete1": "Izdzēst no avota pēc pabeigšanas", "delete2": "Dzēsiet ziņojumus galamērķī, kas nav avotā", - "delete2duplicates": "Dzēst dublikātus galamērķī", + "delete2duplicates": "Izdzēst atkārtojošos vienumus galamērķī", "description": "Apraksts", "domain": "Labot domēnu", "domain_admin": "Labot domēna administratoru", @@ -257,11 +276,11 @@ "hostname": "Saimniekdatora nosaukums", "inactive": "Neaktīvs", "kind": "Veids", - "mailbox": "Rediģēt pastkasti", + "mailbox": "Labot pastkasti", "max_aliases": "Lielākais aizstājvārdu skaits", "max_mailboxes": "Maks. iespējamās pastkastes", "max_quota": "Maks. kvota uz pastkasti (MiB)", - "maxage": "Lielākais ziņojumu, kuri tiks vaicāti attālajā serverī, vecums dienās
    (0 = neņemt vērā vecumu)", + "maxage": "Lielākais pieļaujamais ziņojumu, kuri tiks vaicāti attālajā serverī, vecums dienās
    (0 = neņemt vērā vecumu)", "maxbytespersecond": "Maks. baiti sekundē (0 ir vienāds ar neierobežotu skaitu)", "mins_interval": "Intervāls (min)", "multiple_bookings": "Vairāki rezervējumi", @@ -280,11 +299,11 @@ "sieve_type": "Filtra tips", "skipcrossduplicates": "Izlaist dublētus ziņojumus pa mapēm (pirmais nāk, pirmais kalpo)", "spam_alias": "Izveidot vai mainīt laika ierobežotas aizstājadreses", - "spam_policy": "Pievienot vai noņemt vienumus baltajā-/melnajā sarakstā", - "spam_score": "Iestatīt pielāgotu surogātpasta vērtējumu", + "spam_policy": "Pievienot vai noņemt vienumus atļautajā/liegumu sarakstā", + "spam_score": "Iestatīt pielāgotu mēstules vērtējumu", "subfolder2": "Sinhronizēt galamērķa apakšmapē
    (tukšs = neizmantot apakšmapi)", - "syncjob": "Rediģēt sinhronizācijas darbu", - "target_address": "Iet uz adresi/ēm (komatu atdalītas)", + "syncjob": "Labot sinhronizācijas darbu", + "target_address": "Mērķa adrese/s (atdalītas ar komatu)", "target_domain": "Mērķa domēns", "title": "Labot priekšmetu", "unchanged_if_empty": "Ja neizmainīts atstājiet tukšu", @@ -300,18 +319,25 @@ "sogo_visible": "Aizstājvārds ir redzams SOGo", "sogo_visible_info": "Šī iespēja ietekmē tikai tos objektus, kurus var parādīt SOGo (koplietojamās vai nekoplietojamās aizstājadreses, kas norāda uz vismaz vienu vietējo pastkasti). Ja paslēpts, netiks parādīts SOGo kā atlasāms sūtītājs.", "mbox_rl_info": "Šis pieprasījumu ierobežojums tiek piemērots SASL pieteikšanās vārdam, tas atbilst jebkurai \"from\" adresei, ko izmanto lietotājs, kurš ir pieteicies. Pastkastes pieprasījumu ierobežojums pārraksta domēna mēroga pieprasījumu ierobežojumu.", - "sogo_access": "Nodrošināt tiešu pieteikšanās piekļuvi SOGo", - "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)" + "sogo_access": "Tieša pārvirzīšana uz SOGo", + "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)", + "app_passwd_protocols": "Atļautie lietotnes paroles protokoli", + "allowed_protocols": "Atļautie protokoli tiešai lietotāja piekļuvei (neietekmē lietotnes paroles protokolus)", + "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" }, "footer": { "cancel": "Atcelt", - "confirm_delete": "Apstiprināt dzēšanu", - "delete_now": "Dzēst tagad", + "confirm_delete": "Apstiprināt izdzēšanu", + "delete_now": "Izdzēst tagad", "delete_these_items": "Lūgums apstiprināt izmaiņas šim objekta Id", "loading": "Lūgums uzgaidīt...", "restart_container": "Restartēt konteineri", "restart_container_info": "Svarīgi: nesteidzīga pārsāknēšana var aizņemt ilgāku laiku. Lūgums uzgaidīt, līdz tā tiek pabeigta.", - "restart_now": "Pārsāknēt tagad" + "restart_now": "Pārsāknēt tagad", + "hibp_nok": "Sakrīt. Šī, iespējams, ir bīstama parole." }, "header": { "administration": "Konfigurācija un informācija", @@ -374,7 +400,7 @@ "domain_quota_total": "Kopējais domēna ierobežojums", "domains": "Domēns", "edit": "Labot", - "empty": "Nav rezultātu", + "empty": "Nav iznākuma", "excludes": "Izslēdzot", "filter_table": "Filtra tabula", "filters": "Filtri", @@ -433,13 +459,15 @@ "add_alias_expand": "Izvērst aizstājvārdu pār aizstājdomēniem", "alias_domain_alias_hint": "Aizstājvārdi netiek automātiski piemēroti domēnu aizstājvārdiem. Aizstājadrese my-alias@domain nenosedz adresi my-alias@alias-domain (kur \"alias-domain\" ir iedomāts \"domain\" aizstājdomēns).
    Lūgums izmantot sieta atlasi, lai pārvirzītu pastu uz ārēju pastkasti (skatīt cilti \"Atlasīšana\" vai izmantot SOGo -> Pārsūtītājs). \"Izvērst aizstājvārdu pār aizstājdomēniem\" ir izmantojams, lai automātiski pievienotu trūkstošos aiztājvārdus.", "alias_domain_backupmx": "Aizstājdomēns ir neaktīvs retranslācijas domēnam", - "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)" + "disable_login": "Neļaut pieteikšanos (ienākošais pasts joprojām tiks pieņemts)", + "sieve_preset_1": "Atmest e-pasta vēstules ar iespējami bīstamiem datņu veidiem", + "syncjob_last_run_result": "Pēdējās izpildes iznākums" }, "quarantine": { "action": "Darbības", "atts": "Pielikumi", - "check_hash": "Meklēt faila hašu @ VT", - "empty": "Nav rezultātu", + "check_hash": "Meklēt datnes jaucējvērtību @ VT", + "empty": "Nav iznākuma", "qid": "Rspamd QID", "qitem": "Karantīnas vienumi", "quarantine": "Karantīna", @@ -448,7 +476,7 @@ "received": "Saņemtie", "recipients": "Adresāts", "release": "Atbrīvot", - "release_body": "Šim ziņojumam mēs esam pievienojuši jūsu ziņojumu kā eml failu.", + "release_body": "Mēs pievienojām Tavu ziņojumu kā .eml datni šim ziņojumam.", "release_subject": "Potenciāli kaitīgs karantīnas vienums %s", "remove": "Noņemt", "sender": "Sūtītājs (SMTP)", @@ -458,8 +486,14 @@ "text_plain_content": "Saturs (teksts/vienkāršs)", "toggle_all": "Pārslēgt visu", "disabled_by_config": "Pašreizējā sistēmas konfigurācija atspējo karantīnu. Lūgums iestatīt \"saglabāšanu katrai pastkastītei\" un \"lielākais pieļaujamais lielums\" karantīnas vienumiem.", - "qhandler_success": "Pieprasījums veiksmīgi nosūtīts sistēmai. Tagad var aizvērt logu.", - "qinfo": "Karantīnas sistēma datubāzē saglabās noraidīto pastu (sūtītājam netiks radīts iespaids par piegādātu pastu), kā arī pastu, kas tiek piegādāts kā kopija pastkastes mēstuļu mapē.\n
    \"Apgūt kā surogātpastu un izdzēst\" apgūs ziņojumu kā surogātpastu ar Bajesa teorēmu un aprēķinās arī nestriktas jaucējvērtības, lai nākotnē noraidītu līdzīgus ziņojumus.\n
    Lūgums apzināties, ka vairāku ziņojumu apgūšana var būt laikietilpīga atkarībā no sistēmas.
    Melnā saraksta vienumi karantīnā netiek iekļauti." + "qhandler_success": "Pieprasījums sekmīgi nosūtīts sistēmai. Logu tagad var aizvērt.", + "qinfo": "Karantīnas sistēma datubāzē saglabās noraidīto pastu (sūtītājam netiks radīts iespaids par piegādātu pastu), kā arī pastu, kas tiek piegādāts kā kopija pastkastes mēstuļu mapē.\n
    \"Apgūt kā surogātpastu un izdzēst\" apgūs ziņojumu kā surogātpastu ar Bajesa teorēmu un aprēķinās arī nestriktas jaucējvērtības, lai nākotnē noraidītu līdzīgus ziņojumus.\n
    Lūgums apzināties, ka vairāku ziņojumu apgūšana var būt laikietilpīga atkarībā no sistēmas.
    Lieguma saraksta vienumi karantīnā netiek iekļauti.", + "danger": "Bīstamība", + "notified": "Paziņots", + "refresh": "Atsvaidzināt", + "rspamd_result": "Rspamd iznākums", + "settings_info": "Lielākais pieļaujamais karantējamo vienumu daudzums: %s
    Lielākais pieļaujamais e-pasta lielums: %s MiB", + "spam_score": "Novērtējums" }, "queue": { "queue_manager": "Rindas pārvaldnieks", @@ -490,8 +524,8 @@ "f2b_modified": "Fail2ban parametru izmaiņas tika saglabātas", "forwarding_host_added": "Pāradresācijas hosts %s pievienotsd", "forwarding_host_removed": "Pāradresācijas hosts %s noņemts", - "item_deleted": "Vērtība %s veiksmīgi dzēsta", - "items_deleted": "Vērtība %s veiksmīgi dzēsta", + "item_deleted": "Vienums %s izdzēsts sekmīgi", + "items_deleted": "Vienums %s izdzēsts sekmīgi", "items_released": "Atlasītie vienumi tika izlaisti", "mailbox_added": "Pastkaste %s ir pievienota", "mailbox_modified": "Izmaiņas pastkastei %s ir saglabātas", @@ -504,18 +538,21 @@ "resource_modified": "Izmaiņas %s ir saglabātas", "resource_removed": "Resurs %s tika noņemts", "ui_texts": "Saglabāt UI izmaiņas tekstiem", - "upload_success": "Faila augšupielāde veiksmīga", + "upload_success": "Datne sekmīgi augšupielādēta", "verified_fido2_login": "Apliecināta FIDO2 pieteikšanās", "verified_webauthn_login": "Apliecināta WebAuthn pieteikšanās", "verified_totp_login": "Apliecināta TOTP pieteikšanās", - "verified_yotp_login": "Apliecināta Yubico OTP pieteikšanās" + "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." }, "tfa": { "api_register": "%s izmanto Yubico Cloud API. Lūdzu iegūstiet API atslēgu priekš Jūsu atslēgashere", "confirm": "Apstiprināt", - "confirm_totp_token": "Lūdzu apstipriniet Jūsu izmaiņas ievadot uzģenerēto tekstu", + "confirm_totp_token": "Lūgums apstiprināt savas izmaiņas ar izveidotās tekstvienības ievadīšanu", "delete_tfa": "Atspējot TFA", - "disable_tfa": "Atspējot TFA līdz nākamajai veiksmīgajai pieteikšanās reizei", + "disable_tfa": "Atspējot TFA līdz nākamajai sekmīgajai pieteikšanās reizei", "enter_qr_code": "TOTP kods, ja Tava ierīce nevar nolasīt kvadrātkodus", "key_id": "Jūsu YubiKey identifikators", "key_id_totp": "Identifikators Jūsu atslēgai", @@ -523,11 +560,11 @@ "scan_qr_code": "Lūdzu, skenējiet šo kodu ar savu autentifikācijas lietojumprogrammu vai ievadiet kodu manuāli.", "select": "Lūdzu izvēlaties", "set_tfa": "Uzstādīt difi faktoru autentifik;acijas metodi", - "tfa": "Divu faktoru autentifikācija", + "tfa": "Divpakāpju pieteikšanās", "totp": "Uz laiku bāzēta vienreizēja parole (Google Autentifikātors utt.)", "webauthn": "WebAuthn autentifikācija", "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ūdzu augšā ievadiet Jūsu paroli un apstipriniet WebAuthn reģistrāciju nospiežot 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" }, @@ -572,7 +609,7 @@ "new_password_repeat": "Paroles apstiprinājums (atkārtoti)", "no_active_filter": "Nav pieejami aktīvi filtri", "no_record": "Nav ieraksta", - "password_now": "Pašreizējā parole (Apstiprināt izmaiņas)", + "password_now": "Pašreizējā parole (apstiprināt izmaiņas)", "remove": "Noņemt", "running": "Darbojas", "save_changes": "Saglabāt izmaiņas", @@ -582,11 +619,11 @@ "spam_aliases": "Pagaidu e-pasta aizstājvārdi", "spamfilter": "Mēstuļu filtrs", "spamfilter_behavior": "Reitings", - "spamfilter_bl": "Melnais saraksts", - "spamfilter_bl_desc": "No melnajā sarakstā iekļautajām e-pasta adresēm saņemtās vēstules vienmēr tiks atzīmētas kā mēstules un noraidītas. Noraidītais pasts netiks ievietots karantīnā. Var izmantot aizstājzīmes. Atlasīšana tiek pielietota tikai tiešiem aizstājvārdiem (aizstājvārdiem ar vienu mērķa pastkasti), izņemot visu tverošos aizstājvārdus un pašu pastkasti.", + "spamfilter_bl": "Liegumu saraksts", + "spamfilter_bl_desc": "No lieguma sarakstā iekļautajām e-pasta adresēm saņemtās vēstules vienmēr tiks atzīmētas kā mēstules un noraidītas. Noraidītais pasts netiks ievietots karantīnā. Var izmantot aizstājzīmes. Atlasīšana tiek pielietota tikai tiešiem aizstājvārdiem (aizstājvārdiem ar vienu mērķa pastkasti), izņemot visu tverošos aizstājvārdus un pašu pastkasti.", "spamfilter_default_score": "Noklusējuma vērtības", "spamfilter_green": "Zaļš: šī nav mēstule", - "spamfilter_hint": "Pirmā vērtība norāda uz zemu \"Spam vērtējumu\" vērtējumu, otra vērtība par \"Augstu spam vērtējumu\".", + "spamfilter_hint": "Pirmā vērtība norāda uz zemu \"mēstules novērtējumu\", otrā atspoguļo \"augstu mēstules novērtējumu\".", "spamfilter_red": "Sarkans: Šī vēstule noteikti ir spams un tiek nekavējoties noraidīta", "spamfilter_table_action": "Darbība", "spamfilter_table_add": "Pievienot vienību", @@ -594,8 +631,8 @@ "spamfilter_table_empty": "Nav datu ko parādīt", "spamfilter_table_remove": "noņemt", "spamfilter_table_rule": "Noteikums", - "spamfilter_wl": "Baltais saraksts", - "spamfilter_wl_desc": "No baltā saraksta e-pasta adresēm saņemtās vēstules nekad netiks atzīmētas kā mēstules. Var tikt izmantotas aizstājzīmes. Atlase tiek piemērota tikai tiešiem aizstājvārdiem (aizstājvārdiem ar vienu mērķa pastkasti), izņemot visu tverošos aizstājvārdus un pašu pastkasti.", + "spamfilter_wl": "Atļautais saraksts", + "spamfilter_wl_desc": "No atļautā saraksta e-pasta adresēm saņemtās vēstules nekad netiks atzīmētas kā mēstules. Var tikt izmantotas aizstājzīmes. Atlase tiek piemērota tikai tiešiem aizstājvārdiem (aizstājvārdiem ar vienu mērķa pastkasti), izņemot visu tverošos aizstājvārdus un pašu pastkasti.", "spamfilter_yellow": "Dzeltens: šī vēstule visticamāk ir spams un tiks pārvietota uz Junk mapi", "status": "Status", "sync_jobs": "Sinhronizācijas uzdevumi", @@ -618,18 +655,30 @@ "apple_connection_profile_mailonly": "Šis savienojuma profils iekļauj IMAP un SMTP konfigurācijas parametrus Apple ierīcei.", "pushover_info": "Pašpiegādes paziņojumu iestatījumi attieksies uz visu tīro (ne surogātpasta) pastu, kas piegādāts uz %s, ieskaitot aizstājvārdus (kopīgotus, nekopīgotus, ar birkām).", "app_hint": "Lietotņu paroles ir aizstājējparoles, lai pieteiktos IMAP, SMTP, CalDAV, CardDAV un EAS. Lietotājvārds paliek nemainīgs. SOGo tīmekļa pasts nav pieejams ar lietotņu parolēm.", - "direct_protocol_access": "Šim pastkastes lietotājam ir tieša, ārēja piekļuve zemāk uzskaitītajiem protokoliem un lietotnēm. Šo iestatījumu pārrauga pārvaldītājs. Lietotņu paroles var izveidot, lai nodrošinātu piekļuvi atsevišķiem protokoliem un lietotnēm.
    Poga \"Pieteikties tīmekļa pastā\" nodrošina vienotu pieteikšanos SOGo un vienmēr ir pieejama.", + "direct_protocol_access": "Šim pastkastes lietotājam ir tieša, ārēja piekļuve zemāk uzskaitītajiem protokoliem un lietotnēm. Šo iestatījumu pārrauga pārvaldītājs. Lietotņu paroles var izveidot, lai nodrošinātu piekļuvi atsevišķiem protokoliem un lietotnēm.
    Poga \"Tīmekļa pasts\" nodrošina vienotu pieteikšanos SOGo un vienmēr ir pieejama.", "last_ui_login": "Pēdējā pieteikšanās saskarnē", "login_history": "Pieteikšanās vēsture", "no_last_login": "Nav informācijas par pēdējām pieteikšanās saskarnē reizēm", - "open_webmail_sso": "Pieteikšanās tīmekļa pastā", - "last_mail_login": "Pēdējā pasta pieteikšanās" + "open_webmail_sso": "Tīmekļa pasts", + "last_mail_login": "Pēdējā pasta pieteikšanās", + "change_password_hint_app_passwords": "Kontā ir %d lietotņu paroles, kas netiks mainītas. Lai pārvaldītu tās, jādodas uz cilni \"Lietotņu paroles\".", + "with_app_password": "ar lietotnes paroli", + "apple_connection_profile_with_app_password": "Jauna lietotnes parole ir izveidota un pievienota profilam, lai ierīces iestatīšanas laikā nebūtu nepieciešams ievadīt paroli. Lūgums nekopīgot datni, jo tā nodrošina pilnu piekļuvi pastkastei.", + "tfa_info": "Divpakāpju autentificēšanās palīdz aizsargāt kontu.Ja tā ir iespējota, ir nepieciešamas lietotņu paroles, lai pieteiktos lietotnēs vai pakalpojumos, kas nenodrošina divpakāpju autentificēšanos (piem., e-pasta klienti).", + "app_passwds": "Lietotņu paroles", + "create_app_passwd": "Izveidot lietotnes paroli", + "empty": "Nav iznākuma", + "quarantine_notification_info": "Tiklīdz paziņojums ir nosūtīts, vienumi tiks atzīmēti kā \"paziņoti\", un par šo vienumu vairs netiks sūtīti paziņojumi.", + "sender_acl_disabled": "Sūtītāja pārbaude ir atspējota", + "syncjob_last_run_result": "Pēdējās izpildes iznākums" }, "datatables": { "paginate": { "first": "Pirmā", "last": "Pēdējā" - } + }, + "emptyTable": "Tabulā nav datu", + "search": "Meklēt:" }, "debug": { "last_modified": "Pēdējoreiz mainīts", @@ -657,4 +706,4 @@ "fido2": { "fido2_auth": "Pieteikties ar FIDO2" } -} \ No newline at end of file +} diff --git a/mailcow/data/web/lang/lang.nb-no.json b/mailcow/data/web/lang/lang.nb-no.json index 3b267b9..bca6087 100644 --- a/mailcow/data/web/lang/lang.nb-no.json +++ b/mailcow/data/web/lang/lang.nb-no.json @@ -185,11 +185,12 @@ "protocol_access": "Endre protokolltilgang", "pushover": "Pushover", "quarantine": "Karantenehandlinger", - "quarantine_attachments": "Sett vedlegg i karantene", + "quarantine_attachments": "Se vedlegg i karantene", "quarantine_category": "Endre varslingskategori for karantene", "quarantine_notification": "Endre karantenevarslinger", "domain_desc": "Endre domenebeskrivelse", - "extend_sender_acl": "Tillat utvidelse av sender-ACL fra eksterne adresser" + "extend_sender_acl": "Tillat utvidelse av sender-ACL fra eksterne adresser", + "pw_reset": "Tillat endring av brukerpassord" }, "add": { "app_passwd_protocols": "Tillatte protokoller for app-passord", diff --git a/mailcow/data/web/lang/lang.pl-pl.json b/mailcow/data/web/lang/lang.pl-pl.json index 521c177..41cf102 100644 --- a/mailcow/data/web/lang/lang.pl-pl.json +++ b/mailcow/data/web/lang/lang.pl-pl.json @@ -1,49 +1,117 @@ { "acl": { - "sogo_profile_reset": "Usuń profil SOGo (webmail)", - "syncjobs": "Polecenie synchronizacji", - "alias_domains": "Dodaj aliasy domen" + "sogo_profile_reset": "Zresetuj profil SOGo", + "syncjobs": "Zadania doryczące synchronizacji kont", + "alias_domains": "Dodaj domeny aliasowe", + "delimiter_action": "Akcja oparta na separatorze", + "app_passwds": "Zarządzaj hasłami do aplikacji", + "bcc_maps": "Mapy BCC", + "domain_desc": "Zmień opis dotyczący domeny", + "domain_relayhost": "Zmień serwer przekazujący dla tej domeny", + "eas_reset": "Resetuj urządzenia EAS", + "extend_sender_acl": "Zezwalaj na rozszerzenie listy kontroli dostępu nadawców(ACL) o adresy zewnętrzne", + "filters": "Dostępne filtry", + "login_as": "Zaloguj się jako użytkownik poczty", + "mailbox_relayhost": "Zmień serwer pocztowy(relayhost) dla skrzynki pocztowej", + "prohibited": "Zakazane przez liste kontroli dostępu(ACL)", + "protocol_access": "Zmień dostęp do protokołów", + "pushover": "Pushover(powiadomienia push w czasie rzeczywistym)", + "pw_reset": "Zezwalaj na resetowanie hasła użytkownika mailcow", + "quarantine": "Akcje kwarantanny", + "quarantine_attachments": "Załączniki do kwarantanny", + "quarantine_category": "Zmień kategorię dotyczącą powiadomień o kwarantannie", + "quarantine_notification": "Zmień powiadomienia o kwarantannie", + "ratelimit": "Ograniczenie liczby zapytań", + "recipient_maps": "Mapy odbiorców", + "smtp_ip_access": "Zmiana dozwolonych hostów SMTP", + "sogo_access": "Zezwól na zarządzanie dostępem SOGo", + "spam_alias": "Tymczasowe aliasy", + "spam_policy": "Zablokowane adresy/Dozwolone adresy", + "spam_score": "Wskaźnik spam", + "tls_policy": "Polityka TLS", + "unlimited_quota": "Nieograniczony limit dla skrzynek pocztowych" }, "add": { "active": "Aktywny", "add": "Dodaj", - "alias_address": "Alias/y:", + "alias_address": "Alias/y", "alias_address_info": "Pełny/e adres/y email lub @example.com, aby przejąć wszystkie wiadomości dla domeny (oddzielone przecinkami). tylko domeny mailcow.", "alias_domain": "Alias domeny", "alias_domain_info": "Tylko prawidłowe nazwy domen (oddzielone przecinkami).", - "backup_mx_options": "Opcje Backup MX:", + "backup_mx_options": "Opcje Backup MX", "delete1": "Usuń ze źródła po zakończeniu", "delete2duplicates": "Usuń duplikaty w miejscu docelowym", - "description": "Opis:", + "description": "Opis", "domain": "Domena", - "domain_quota_m": "Łączny limit domeny (MiB):", + "domain_quota_m": "Łączny limit domeny (MiB)", "enc_method": "Metoda szyfrowania", "exclude": "Wyklucz obiekty (regex)", - "full_name": "Pełna nazwa:", + "full_name": "Pełna nazwa", "hostname": "Nazwa hosta", "kind": "Rodzaj", - "mailbox_quota_m": "Maks. wielkość skrzynki (MiB):", - "mailbox_username": "Nazwa użytkownika (lewa strona adresu email):", - "max_aliases": "Maks. liczba aliasów:", - "max_mailboxes": "Maks. liczba skrzynek:", + "mailbox_quota_m": "Maks. wielkość skrzynki (MiB)", + "mailbox_username": "Nazwa użytkownika (lewa strona adresu email)", + "max_aliases": "Maks. liczba aliasów", + "max_mailboxes": "Maks. liczba skrzynek", "mins_interval": "Zakres pobierania (minuty)", "multiple_bookings": "Wielokrotne rejestracje", - "password": "Hasło:", - "password_repeat": "Potwierdź hasło(powtórz):", + "password": "Hasło", + "password_repeat": "Potwierdź hasło(powtórz)", "port": "Port", - "post_domain_add": "Po dodaniu nowej domeny będziesz musiał ponownie uruchomić kontener serwisowy SOGo!", - "quota_mb": "Limit wielkości (MiB):", + "post_domain_add": "Kontener SOGo, \"sogo-mailcow\", musi zostać ponownie uruchomiony po dodaniu nowej domeny!

    Dodatkowo należy przejrzeć konfigurację DNS domeny. Po zatwierdzeniu konfiguracji DNS uruchom ponownie \"acme-mailcow\", aby automatycznie wygenerować certyfikaty dla nowej domeny (autoconfig.<domain>, autodiscover.<domain>).
    Ten krok jest opcjonalny i będzie ponownie wykonywany co 24 godziny.", + "quota_mb": "Limit wielkości (MiB)", "relay_all": "Przekaż wszystkim odbiorcom", - "relay_all_info": "Jeśli decydujesz się nie przekazywać wszystkim odbiorcom, musisz dodać (\"ślepą\")skrzynkę dla każdego poszczególnego odbiorcy, któremu należy przekazać.", + "relay_all_info": "↪Jeśli wybierzesz not, aby przekazać wszystkich odbiorców, musisz dodać (\"ślepą\") skrzynkę pocztową dla każdego pojedynczego odbiorcy, która powinna być przekazywana.", "relay_domain": "Domena przekaźnikowa", "select": "Proszę wybrać...", "select_domain": "Proszę najpierw wybrać domenę", "syncjob": "Dodaj polecenie synchronizacji", "syncjob_hint": "Pamiętaj, że hasła należy zapisywać w zwykłym tekście!", - "target_address": "Adresy Idź do:", + "target_address": "Adresy Idź do", "target_address_info": " Pełny/e adres/y email (oddzielone przecinkami).", - "target_domain": "Domena docelowa:", - "username": "Nazwa użytkownika" + "target_domain": "Domena docelowa", + "username": "Nazwa użytkownika", + "activate_filter_warn": "Wszystkie pozostałe filtry zostaną wyłączone, gdy opcja active zostanie zaznaczona.", + "add_domain_only": "Dodaj wyłącznie domene", + "add_domain_restart": "Dodaj domenę i uruchom ponownie SOGo", + "app_name": "Nazwa aplikacji", + "app_password": "Dodaj hasło do aplikacji", + "app_passwd_protocols": "Dozwolone protokoły dla hasła aplikacji", + "automap": "Spróbuj automatycznie mapować foldery (\"Wysłane elementy\", \"Wysłane\" => \"Wysłane\" itp.)", + "bcc_dest_format": "Miejscem docelowym BCC musi być jeden prawidłowy adres e-mail.
    Jeśli chcesz wysłać kopię do wielu adresów, utwórz alias i użyj go tutaj.", + "comment_info": "Prywatny komentarz nie jest widoczny dla użytkownika, podczas gdy publiczny komentarz jest wyświetlany jako podpowiedź, które pojawia się, gdy użytkownik najedzie myszką nad elementem", + "custom_params": "Niestandardowe parametry", + "custom_params_hint": "Właściwa: --param=xy, błędna: --param xy", + "delete2": "Usuń wiadomości w miejscu docelowym, które nie znajdują się w źródle", + "destination": "Miejsce docelowe", + "disable_login": "Nie pozwalaj na logowanie(poczta przychodząca jest nadal akceptowana)", + "domain_matches_hostname": "Domena pasuje do nazwy hosta", + "dry": "Symulacja synchronizacji", + "gal": "Globalna lista adresów", + "gal_info": "GAL zawiera wszystkie obiekty domeny i nie może być edytowany przez żadnego użytkownika. Wolne/zajęte logi w SOGo bedą nidostępne, jeśli są wyłączone! Uruchom ponownie SOGo, aby zastosować zmiany.", + "generate": "generuj", + "goto_ham": "Ucz się jako ham", + "goto_null": "Odrzucaj pocztę \"po cichu\"", + "goto_spam": "Ucz się jako spam", + "inactive": "Nieaktywny", + "internal": "Wewnętrzny", + "internal_info": "Aliasy wewnętrzne są dostępne tylko z domeny własnej lub domeny aliasów.", + "mailbox_quota_def": "Domyślny przydział skrzynki pocztowej", + "nexthop": "Następny hop", + "private_comment": "Prywatny komentarz", + "public_comment": "Komentarz publiczny", + "relay_transport_info": "
    Info
    Możesz transports maps dla niestandardowego miejsca docelowego dla tej domeny. Jeśli nie jest ustawiony, zostanie aktywowany lookup MX.", + "relay_unknown_only": "Przekaż tylko nieistniejące skrzynki pocztowe. Istniejące skrzynki pocztowe będą dostarczane lokalnie.", + "relayhost_wrapped_tls_info": "Proszę nie używać portów owiniętych TLS (głównie używanych na porcie 465).
    \nUżyj dowolnego portu niezawiniętego i uruchom STARTTLS. Polityka TLS w celu egzekwowania TLS może być utworzona w \"mapie zasad TLS\".", + "sieve_desc": "Krótki opis", + "sieve_type": "Typ filtra", + "skipcrossduplicates": "Pomiń duplikaty wiadomości w folderach (na zasadzie kolejności zgłoszeń)", + "subscribeall": "Subskrybuj wszystkie foldery", + "tags": "Tagi", + "timeout1": "Limit czasu połączenia z hostem zdalnym", + "timeout2": "Limit czasu połączenia dla lokalnego hosta", + "validate": "Zatwierdź", + "validation_success": "Zatwierdzone z powodzeniem" }, "admin": { "access": "Dostęp", @@ -71,7 +139,7 @@ "f2b_max_attempts": "Max. ilość prób", "f2b_parameters": "Parametry Fail2ban", "f2b_retry_window": "Spróbuj ponownie (s) dla max. ilości prób", - "f2b_whitelist": "Biała lista sieci/hosty", + "f2b_whitelist": "Dozwolone sieci/hosty", "filter_table": "Tabela filtru", "forwarding_hosts": "Hosty przekazujące", "forwarding_hosts_add_hint": "Możesz albo wyszczególnić adresy IPv4/IPv6, sieci w notacji CIDR, nazwy hostów (które zostaną rozłożone na adresy IP), albo nazwy domen (które zostaną rozłożone na adresy IP poprzez sprawdzanie rekordów SPF lub, w razie ich braku, rekordów MX).", @@ -99,30 +167,274 @@ "spamfilter": "Filtr spamu", "time": "Czas", "unchanged_if_empty": "W przypadku braku zmian, nie wypełniaj", - "username": "Nazwa użytkownika" + "username": "Nazwa użytkownika", + "activate_api": "Aktywuj API", + "activate_send": "Aktywuj przycisk wysyłania", + "active_rspamd_settings_map": "Aktywne ustawienia mapy", + "add_admin": "Dodaj administratora", + "add_relayhost": "Dodaj transport zależny od nadawcy", + "add_relayhost_hint": "Miej na uwadze, że dane uwierzytelniające, jeśli takie istnieją, będą przechowywane w postaci zwykłego tekstu.", + "add_row": "Dodaj wiersz", + "add_settings_rule": "Dodaj regułę ustawień", + "add_transport": "Dodaj transport", + "add_transports_hint": "Należy pamiętać, że dane uwierzytelniające, jeśli takie istnieją, będą przechowywane jako zwykły tekst.", + "additional_rows": " Dodano kolejne rzędy", + "admins": "Administratorzy", + "admins_ldap": "Administratorzy LDAP", + "admin_quicklink": "Ukryj szybki link do strony logowania administratora", + "advanced_settings": "Ustawienia zaawansowane", + "allowed_methods": "Dostęp-kontrola-zezwolenie-metody", + "allowed_origins": "Dostęp-Kontrola-Zezwolenia-Pochodzenie", + "api_allow_from": "Zezwalaj na dostęp API z tych notacji sieciowych IP/CIDR", + "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_skip_ip_check": "Pomiń sprawdzenie IP dla API", + "app_hide": "Ukryj dla logowania", + "app_links": "Linki aplikacji", + "app_name": "Nazwa aplikacji", + "apps_name": "Nazwa \"aplikacji mailcow\"", + "arrival_time": "Czas serwera", + "authed_user": "Autoryzowany użytkownik", + "ays": "Jesteś pewien, że chcesz kontynuować?", + "ban_list_info": "Zobacz listę zakazanych adresów IP poniżej: network (pozostały czas zakazu) - [działania].
    IP kolejkowane do odbanowania zostaną usunięte z listy aktywnych zakazów w ciągu kilku sekund.
    Czerwone etykiety wskazują aktywny stałe zakazy poprzez odmowę wpisu.", + "change_logo": "Zmień logo", + "logo_normal_label": "Normalna", + "logo_dark_label": "Odwrócony dla trybu ciemnego", + "convert_html_to_text": "Konwertuj HTML na zwykły tekst", + "copy_to_clipboard": "Tekst skopiowany do schowka!", + "cors_settings": "Ustawienia CORS", + "credentials_transport_warning": "Ostrzeżenie: Dodanie nowego wpisu mapy transportu spowoduje aktualizację danych uwierzytelniających dla wszystkich wpisów z pasującą kolumną hop.", + "customer_id": "Identyfikator klienta", + "customize": "Dostosuj", + "login_page": "Strona logowania", + "destination": "Miejsce docelowe", + "dkim_domains_selector": "Selektor", + "dkim_domains_wo_keys": "Wybierz domeny z brakującymi kluczami", + "dkim_from": "Od", + "dkim_from_title": "Domena źródłowa do kopiowania danych z", + "dkim_overwrite_key": "Nadpisanie istniejącego klucza DKIM", + "dkim_to": "Do", + "dkim_to_title": "Domeny docelowe - zostaną nadpisane", + "domain_admin": "Administrator domeny", + "domainadmin_quicklink": "Ukryj szybki link do strony logowania administratora domeny", + "domain_s": "Domena/y", + "duplicate": "Duplikat", + "duplicate_dkim": "Duplikat rekordu DKIM", + "excludes": "Wyklucza tych odbiorców", + "f2b_ban_time_increment": "Czas zakazu jest zwiększany z każdym zakazem", + "f2b_blacklist": "Lista odrzuconych sieci/hostów", + "f2b_filter": "Filtry Regex", + "f2b_list_info": "Odrzucony host lub sieć zawsze będzie przewyższać jednostkę zezwalającą.Zastosowanie aktualizacji listy zajmie kilka sekund.", + "f2b_manage_external": "Zarządzaj Fail2Ban zewnętrznie", + "f2b_manage_external_info": "Fail2ban nadal będzie utrzymywać listę banów, ale nie będzie aktywnie ustalać zasad blokowania ruchu. Użyj wygenerowane listy banów poniżej, aby zewnętrznie zablokować ruch.", + "f2b_max_ban_time": "Max. czas bana (s)", + "f2b_netban_ipv4": "Rozmiar podsieci IPv4 do zastosowania zakazu (8-32)", + "f2b_netban_ipv6": "Rozmiar podsieci IPv6 do zastosowania zakazu (8-128)", + "f2b_regex_info": "Logi brane pod uwagę: SOGo, Postfix, Dovecot, PHP-FPM.", + "filter": "Filtr", + "force_sso_text": "Jeśli skonfigurowany jest zewnętrzny dostawca OIDC, ta opcja ukrywa domyślne formularze logowania mailcow i pokazuje tylko przycisk logowania pojedynczego", + "force_sso": "Wyłącz logowanie mailcow i pokaż tylko pojedyncze logowanie", + "from": "Od", + "generate": "Generuj", + "guid": "GUID - unikalny identyfikator instancji", + "guid_and_license": "GUID & licencja", + "hash_remove_info": "Usunięcie hasha z limitem współczynnika (jeśli nadal istnieje) spowoduje całkowite zresetowanie jego licznika.
    \n\n\n\n Każdy hash jest oznaczony indywidualnym kolorem.", + "help_text": "Zastąp tekst pomocy poniżej maski logowania (dozwolone HTML)", + "html": "HTML", + "iam": "Dostawca tożsamości", + "iam_attribute_field": "Pole atrybutów", + "iam_authorize_url": "Punkt końcowy autoryzacji", + "iam_auth_flow": "Przepływ uwierzytelniania", + "iam_auth_flow_info": "Oprócz przepływu kodu autoryzacyjnego (Standardowy przepływ w Keycloak), który służy do logowania jednokrotnego logowania, mailcow obsługuje również przepływ uwierzytelniania z bezpośrednimi poświadczeniami. Mailpassword Flow próbuje zweryfikować dane uwierzytelniające użytkownika za pomocą Keycloak Admin REST API. mailcow pobiera hashowane hasło z atrybutu mailcow_password, który jest mapowany w Keycloak.", + "iam_basedn": "Baza DN", + "iam_client_id": "ID klienta", + "iam_client_secret": "Sekret klienta", + "iam_client_scopes": "Zakresy klientów", + "iam_default_template": "Domyślny szablon", + "iam_default_template_description": "Jeśli użytkownikowi nie zostanie przypisany żaden szablon, domyślny szablon zostanie użyty do utworzenia skrzynki pocztowej, ale nie do aktualizacji skrzynki pocztowej.", + "iam_description": "Skonfiguruj zewnętrznego dostawce uwierzytelniania
    Skrzynki pocztowe użytkownika zostaną automatycznie utworzone przy pierwszym logowaniu, pod warunkiem, że zostało ustawione mapowanie atrybutów.", + "iam_extra_permission": "Aby następujące ustawienia działały, klient mailcow w Keycloak potrzebuje konta serwisowego oraz uprawnień do podgląd-użytkowników.", + "iam_host": "Host", + "iam_host_info": "Wprowadź jeden lub więcej hostów LDAP, oddzielonych przecinkami.", + "iam_import_users": "Zaimportuj użytkowników", + "iam_login_provisioning": "Automatyczne tworzenie użytkowników przy logowaniu", + "iam_mapping": "Mapowanie atrybutów", + "iam_bindpass": "Powiąż hasło", + "iam_periodic_full_sync": "Okresowa pełna synchronizacja", + "iam_port": "Port", + "iam_realm": "Realm", + "iam_redirect_url": "Przekierowanie Url", + "iam_rest_flow": "Procedura uwierzytelniania hasła pocztowego", + "iam_server_url": "Adres URL serwera", + "iam_sso": "Pojedyncze logowanie", + "iam_sync_interval": "Interwał synchronizacji / importu (min)", + "iam_test_connection": "Test połączenia", + "iam_token_url": "Tokenowy punkt końcowy", + "iam_userinfo_url": "Endpoint informacji o użytkowniku", + "iam_username_field": "Pole nazwy użytkownika", + "iam_binddn": "Powiąź DN", + "iam_use_ssl": "Używaj SSL", + "iam_use_ssl_info": "Jeśli włączysz SSL, a port zostanie ustawiony na 389, zostanie automatycznie nadpisany, aby użyć 636.", + "iam_use_tls": "Używaj StartTLS", + "iam_use_tls_info": "Jeśli włączono TLS, należy użyć domyślnego portu dla serwera LDAP (389). Nie można używać portów SSL.", + "iam_version": "Wersja", + "ignore_ssl_error": "Ignoruj błędy SSL", + "in_use_by": "W użyciu przez", + "include_exclude": "Uwzględnij/Nie uwzględniaj", + "include_exclude_info": "Domyślnie - bez zaznaczenia - wszystkie skrzynki pocztowe są adresowane", + "includes": "Uwzględnij tych odbiorców", + "ip_check": "Sprawdź IP", + "ip_check_disabled": "Sprawdzenie IP jest wyłączone. Możesz go włączyć w obszarze
    System > Konfiguracja > Opcje > Dostosuj", + "ip_check_opt_in": "Opt-In korzystania z usług stron trzecich ipv4.mailcow.email i ipv6.mailcow.email w celu rozwiązania zewnętrznych adresów IP.", + "is_mx_based": "Bazuje na MX", + "last_applied": "ostatnio zastosowany", + "license_info": "Licencja nie jest wymagana, ale pomaga w dalszym rozwoju.
    Zarejestruj swój GUID tutaj lub kup wsparcie dla instalacji mailcow.", + "link": "Link", + "login_time": "Cześć logowania", + "logo_info": "Twój obraz zostanie przeskalowany do wysokości 40px dla górnego paska nawigacyjnego i max. szerokości 250px dla strony startowej. Zalecana jest skalowalna grafika.", + "recipients": "Odbiorcy", + "send": "Wyślij", + "sender": "Nadawca", + "lookup_mx": "Destynacja jest regularnym wyrażeniem, które pasuje do nazwy MX (.*\\\\.google\\.com, aby przekierować całą pocztę skierowaną do MX kończącego się na google.com w tym hopie)", + "main_name": "Nazwa \"mailcow UI\"", + "merged_vars_hint": "Wyszarzone wiersze zostały połączone z vars. (local.) inc.php i nie można ich modyfikować.", + "message_size": "Rozmiar wiadomości", + "nexthop": "Następny hop", + "needs_restart": "potrzebny restart", + "no": "✕", + "no_active_bans": "Brak aktywnych banów", + "no_new_rows": "Brak dostępnych kolejnych wierszy", + "oauth2_apps": "Aplikacje OAuth2", + "oauth2_add_client": "Dodaj klienta OAuth2", + "oauth2_client_id": "ID klienta", + "oauth2_client_secret": "Sekret klienta", + "oauth2_info": "Implementacja OAuth2 obsługuje typ grantu \"Kod autoryzacji\" i wydaje tokeny odświeżania.
    \nSerwer automatycznie wydaje również nowe tokeny odświeżania, po użyciu tokena odświeżania.

    \n• Domyślnym zakresem jest profil. Tylko użytkownicy skrzynek pocztowych mogą być uwierzytelniani w OAuth2. Jeśli parametr zakresu zostanie pominięty, wraca do profil.
    \n\nParametr state musi zostać wysłany przez klienta w ramach żądania autoryzacji.

    >\nŚcieżki zapytań do API OAuth2:
    \n
      \nPunkt końcowy autoryzacji\n/oauth/authorize
    • Token endpoint: /oauth/token
    • Resource page: /oauth/profile
    • \n
    \nRegeneracja tajemnicy klienta nie spowoduje wygaśnięcia istniejących kodów autoryzacyjnych, ale nie odnowi ich tokena.

    \nOdwołanie tokenów klienta spowoduje natychmiastowe zakończenie wszystkich aktywnych sesji. Wszyscy klienci muszą się ponownie uwierzytelnić.", + "oauth2_redirect_uri": "Przekieruj URI", + "oauth2_renew_secret": "Wygeneruj sekret nowego klienta", + "oauth2_revoke_tokens": "Unieważnij wszystkie tokeny klienta", + "optional": "Opcjonalny", + "options": "Opcje", + "password_length": "Długość hasła", + "password_policy": "Polityka haseł", + "password_policy_chars": "Musi zawierać co najmniej jeden znak alfabetyczny", + "password_policy_length": "Minimalna długość hasła to %d", + "password_policy_lowerupper": "Musi zawierać małe i duże litery", + "password_policy_numbers": "Musi zawierać co najmniej jeden numer", + "password_policy_special_chars": "Musi zawierać znaki specjalne", + "password_reset_info": "Jeśli nie jest udostępniony e-mail zapasowy, ta funkcja nie może być używana.", + "password_reset_settings": "Ustawienia odzyskiwania haseł", + "password_reset_tmpl_html": "Szablon HTML", + "password_reset_tmpl_text": "Szablon tekstu", + "password_settings": "Ustawienia hasła", + "quarantine_bcc": "Wyślij kopię wszystkich powiadomień (BCC) do tego odbiorcy:
    Pozostaw pustą aby wyłączyć. Niepodpisana, niezaznaczona poczta. Powinna być dostarczana tylko wewnętrznie.", + "quarantine_exclude_domains": "Wyklucz domeny i domeny aliasowe", + "quarantine_max_age": "Maksymalny wiek w dniach
    Wartość musi być równa lub większa niż 1 dzień.", + "quarantine_max_score": "Odrzuć powiadomienie, jeśli wskaźnik spamu wiadomości jest wyższy niż ta wartość:
    Domyślne do 9999.0", + "quarantine_max_size": "Maksymalny rozmiar w MiB (większe elementy są odrzucane):
    0 oznacza, że nie oznacza nieograniczony.", + "quarantine_notification_html": "Szablon wiadomości e-mail z powiadomieniem:
    Pozostaw puste, aby przywrócić szablon domyślny.", + "quarantine_notification_sender": "Powiadomienie nadawcy wiadomości e-mail", + "quarantine_notification_subject": "Powiadomienie E-mail Temat", + "quarantine_redirect": "Przekieruj wszystkie powiadomienia do tego odbiorcy:
    Pozostaw pusty aby wyłączyć. Niepodpisana, niezaznaczona poczta. Powinny być dostarczane tylko wewnętrznie.", + "quarantine_release_format": "Format zwolnionych przedmiotów", + "quarantine_release_format_att": "Jako załącznik", + "quarantine_release_format_raw": "Niezmodyfikowany oryginał", + "quarantine_retention_size": "Retencje na skrzynkę pocztową:
    0 oznacza nieaktywne.", + "quicklink_text": "Pokaż lub ukryj szybkie linki do innych stron logowania pod formularzem logowania", + "quota_notification_html": "Szablon wiadomości e-mail z powiadomieniem:
    Pozostaw puste, aby przywrócić szablon domyślny.", + "quota_notification_sender": "Powiadomienie nadawcy wiadomości e-mail", + "quota_notification_subject": "Temat powiadomienia e-mail", + "quota_notifications": "Powiadomienia o limitach", + "quota_notifications_info": "Powiadomienia o limktach są wysyłane do użytkowników raz przy przekroczaniu 80% i raz przy przekraczaniu 95% zużycia.", + "quota_notifications_vars": "{{percent}} równa się aktualnemu limitowi użytkownika
    {{username}} jest nazwą skrzynki pocztowej", + "queue_unban": "Zdejmij bana", + "rate_name": "Nazwa oceny", + "regen_api_key": "Regeneruj klucz API", + "regex_maps": "Mapy Regex", + "relay_from": "\"Od:\" adres", + "relay_rcpt": "\"Do:\"Adres", + "relay_run": "Uruchom test", + "relayhosts": "Transporty zależne od nadawcy", + "relayhosts_hint": "Zdefiniuj transporty zależne od nadawcy, aby móc wybrać je w oknie dialogowym konfiguracji domeny.
    \nUsługa transportu jest zawsze \"smtp:\" i dlatego uruchomj TLS, gdy będzie oferowana. Owinięty TLS (SMTPS) nie jest obsługiwany. Pod uwagę brane jest indywidualne ustawienie polityki wychodzącej TLS użytkownika.
    \nDotyczy wybranych domen, w tym domen aliasowych.", + "remove_row": "Usuń wiersz", + "reset_default": "Zresetuj do ustawień domyślnych", + "reset_limit": "Usuń hash", + "reset_password_vars": "{{link}} Wygenerowany link do resetu hasła
    {{username}} Nazwa skrzynki pocztowej użytkownika, który poprosił o zresetowanie hasła
    {{username2}} Nazwa skrzynki pocztowej do odzyskiwania
    {{date}} Data złożenia żądania resetowania hasła
    {{token_lifetime}}Żywotność tokena w minutach
    {{hostname}} Nazwa hosta mailcow", + "restore_template": "Pozostaw puste, aby przywrócić szablon domyślny.", + "routing": "Routowanie", + "rsetting_add_rule": "Dodaj regułę", + "rsetting_content": "Zawartość reguły", + "rsetting_desc": "Krótki opis", + "rsetting_no_selection": "Proszę zaznaczyć regułę", + "rsetting_none": "Brak dostępnych reguł", + "rsettings_insert_preset": "Wstaw przykładowy preset", + "rsettings_preset_1": "Wyłącz wszystkie z wyjątkiem DKIM i limitu stawek dla uwierzytelnionych użytkowników", + "rsettings_preset_2": "Administratorzy poczty pozwalają na spam", + "rsettings_preset_3": "Zezwalaj tylko określonym nadawcom na skrzynkę pocztową (tj. używaj tylko jako wewnętrznej skrzynki pocztowej)", + "rsettings_preset_4": "Wyłącz Rspamd dla domeny", + "rspamd_com_settings": "Nazwa ustawienia zostanie automatycznie wygenerowana, zobacz przykładowe ustawienia wstępne poniżej. Aby uzyskać więcej informacji, zobacz Rspamd docs", + "rspamd_global_filters": "Globalne mapy filtrów", + "rspamd_global_filters_agree": "Będę ostrożny!", + "rspamd_global_filters_info": "Globalne mapy filtrów zawierają różne rodzaje globalnych list dozwolonych i zablokowanych adresów.", + "rspamd_global_filters_regex": "Ich imiona wyjaśniają ich cel. Cała zawartość musi zawierać ważne wyrażenie regularne w formacie \"/pattern/options\" (e.g. /.+@domain\\.tld/i).
    \nChociaż podstawowe kontrole są wykonywane na każdej linii regex, funkcja Rspamd może zostać złamana, jeśli nie odczyta poprawnie składni.
    >\nRspamd po zmianie spróbuje odczytać zawartość mapy. Jeśli wystąpią problemy, restart Rspamd, aby wymusić ponowne ładowanie mapy.
    Elementy z listy odrzuconych są wyłączone z kwarantanny.", + "rspamd_settings_map": "Mapa ustawień Rspamd", + "sal_level": "Poziom Moo", + "service": "Usługa", + "service_id": "Id usługi", + "subject": "Temat", + "success": "Sukces", + "sys_mails": "Maile systemowe", + "task": "Zadanie", + "text": "Tekst", + "title": "Tytuł", + "title_name": "Tytuł strony internetowej \"mailcow UI\"", + "to_top": "Powrót na górę", + "transport_dest_format": "Regex lub syntax: example.org, .example.org, *, box@example.org(wiele wartości można rozdzielić przecinkami)", + "transport_maps": "Mapy transportu", + "transport_test_rcpt_info": "• Użyj null@hosted.mailcow.de aby przetestować przekazywanie do nieznanego miejsca docelowego.", + "transports_hint": "• Wpis mapy transportowej overrules mapa transportowa zależna od nadawcy.
    \nNajlepiej stosować transporty oparte na MX •.
    \nUstawienia zasad Outbound TLS dotyczące użytkownika są ignorowane i mogą być egzekwowane tylko przez wpisy mapy zasad TLS\n• Usługa transportu dla zdefiniowanych transportów jest zawsze \"smtp:\" i dlatego uruchomi TLS, gdy będzie oferowana. Wrapped TLS (SMTPS) nie jest obsługiwany.
    \nAdresy pasujące do \"/localhost$/\" będą zawsze transportowane przez \"lokalne:\", dlatego miejsce docelowe \"*\" nie będzie miało zastosowania do tych adresów.
    \n• Aby określić poświadczenia dla przykładowego następnego hopa \"[host]:25\", Postfix zawsze kolejkuje zapytania o \"host\" przed wyszukaniem \"[host]:25\". Takie zachowanie uniemożliwia jednoczesne użycie \"host\" i \"[host]:25.", + "ui_footer": "Stopka (dozwolone HTML)", + "ui_header_announcement": "Ogłoszenia", + "ui_header_announcement_active": "Ustaw ogłoszenie jako aktywne", + "ui_header_announcement_content": "Tekst (dozwolony HTML)", + "ui_header_announcement_help": "Ogłoszenie jest widoczne dla wszystkich zalogowanych użytkowników oraz na ekranie logowania interfejsu użytkownika.", + "ui_header_announcement_select": "Wybierz typ ogłoszenia", + "ui_header_announcement_type": "Typ", + "ui_header_announcement_type_danger": "Bardzo ważne", + "ui_header_announcement_type_info": "Info", + "ui_header_announcement_type_warning": "Ważne", + "ui_texts": "Etykiety i teksty UI", + "unban_pending": "oczekuje na odblokowanie", + "upload": "Prześlij", + "user_link": "Link użytkownika", + "user_quicklink": "Ukryj Quicklink do strony logowania użytkownika", + "validate_license_now": "Sprawdź identyfikator GUID względem serwera licencji", + "verify": "Zweryfikuj", + "yes": "✓" }, "danger": { "access_denied": "Odmowa dostępu lub nieprawidłowe dane w formularzu", "alias_domain_invalid": "Alias domeny nieprawidłowy", "alias_empty": "Alias nie może być pusty", "alias_goto_identical": "Alias i Idź do nie mogą być identyczne", - "alias_invalid": "Alias nieprawidłowy", + "alias_invalid": "Adres aliasu jest nieprawidłowy", "aliasd_targetd_identical": "Alias domeny nie może być identyczny z domeną docelową", "aliases_in_use": "Maks. liczba aliasów musi być większa od lub równa %d", "description_invalid": "Nieprawidłowy opis źródła", "dkim_domain_or_sel_invalid": "Nieprawidłowa domena lub selektor DKIM", - "domain_exists": "Domena %s już istnieje", - "domain_invalid": "Błędna nazwa domeny", + "domain_exists": "Domena już istnieje", + "domain_invalid": "Nazwa domeny jest pusta lub nieprawidłowa", "domain_not_empty": "Nie można usunąć niepustej domeny", - "domain_not_found": "Nie znaleziono domeny %s", - "domain_quota_m_in_use": "Limit domeny %s MiB", - "goto_empty": "Adres Idź do nie może być pusty", + "domain_not_found": "Nie znaleziono domeny", + "domain_quota_m_in_use": "Limit domeny musi być większy lub równy do MiB", + "goto_empty": "Adres alias musi zawierać co najmniej jeden prawidłowy adres goto", "goto_invalid": "Adres Idź do jest nieprawidłowy", - "is_alias": "%s został już podany jako alias", - "is_alias_or_mailbox": "%s podano wcześniej jako alias lub skrzynkę", - "is_spam_alias": "%s podano wcześniej jako alias dla spam", - "last_key": "Nie można usunąć ostatniego klucza", - "login_failed": "Niepowodzenie logowania", + "is_alias": "został już podany jako alias", + "is_alias_or_mailbox": "jest już znany jako alias, skrzynka pocztowa lub adres aliasu rozwinięty z domeny aliasu.", + "is_spam_alias": "jest już znany jako tymczasowy adres aliasu (adres aliasu antyspamowego)", + "last_key": "Nie można usunąć ostatniego klucza, zamiast tego należy wyłączyć TFA.", + "login_failed": "Logowanie nie powiodło się", "mailbox_invalid": "Nieprawidłowa nazwa skrzynki", "mailbox_quota_exceeded": "Wielkość przekracza limit domeny (maks. %d MiB)", "mailbox_quota_exceeds_domain_quota": "Maksymalna wielkość przekracza limit domeny", @@ -144,8 +456,107 @@ "sender_acl_invalid": "ACL Nadawcy jest nieprawidłowy", "target_domain_invalid": "Domena Idź do jest nieprawidłowa", "targetd_not_found": "Nie znaleziono domeny docelowej", - "username_invalid": "Nie można użyć nazwy użytkownika", - "validity_missing": "Proszę wyznaczyć termin ważności" + "username_invalid": "Nie można użyć tej nazwy użytkownika", + "validity_missing": "Proszę wyznaczyć termin ważności", + "to_invalid": "Pole odbiorca nie może być puste", + "app_name_empty": "Nazwa aplikacji nie może być pusta", + "app_passwd_id_invalid": "Identyfikator hasła aplikacji jest niepoprawny", + "authsource_in_use": "Nie można zmienić ani usunąć dostawcy tożsamości, ponieważ jest używany przez co najmniej jednego użytkownika.", + "bcc_empty": "Miejsce docelowe BCC nie może być puste", + "bcc_exists": "Mapa BCC istnieje już dla tego typu", + "bcc_must_be_email": "Miejsce docelowe BCC nie jest prawidłowym adresem e-mail", + "comment_too_long": "Komentarz zbyt długi, maksymalnie 160 dozwolonych znaków", + "cors_invalid_method": "Podano nieprawidłową metodę Allow-Method", + "cors_invalid_origin": "Podano nieprawidłową wartość Allow-Origin", + "defquota_empty": "Domyślny limit skrzynki pocztowej nie może wynosić 0.", + "demo_mode_enabled": "Tryb demo jest włączony", + "dkim_domain_or_sel_exists": "Klucz DKIM już istnieje i nie zostanie nadpisany", + "domain_cannot_match_hostname": "Domena nie może być taka sama jak nazwa hosta", + "extended_sender_acl_denied": "brak ACL do ustawiania adresów nadawcy zewnętrznego", + "extra_acl_invalid": "Adres nadawcy zewnętrznego jest nieprawidłowy", + "extra_acl_invalid_domain": "Nadawca zewnętrzny używa nieprawidłowej domeny", + "fido2_verification_failed": "Weryfikacja FIDO2 nie powiodła się", + "file_open_error": "Nie można otworzyć pliku do zapisu", + "filter_type": "Niewłaściwy typ filtra", + "from_invalid": "Nadawca nie może być pusty", + "generic_server_error": "Wystąpił nieoczekiwany błąd serwera. Skontaktuj się z administratorem.", + "global_filter_write_error": "Nie można zapisać pliku filtra", + "global_map_invalid": "Id mapy globalnej nieprawidłowe", + "global_map_write_error": "Nie można zapisać globalnej mapy ID", + "ham_learn_error": "Błąd uczenia ham", + "iam_test_connection": "Połączenie nie powiodło się", + "imagick_exception": "Błąd: wyjątek Imagick podczas odczytu obrazu", + "img_dimensions_exceeded": "Obraz przekracza maksymalny rozmiar obrazu", + "img_invalid": "Nie można zweryfikować pliku obrazu", + "img_size_exceeded": "Obraz przekracza maksymalny rozmiar pliku", + "img_tmp_missing": "Nie można zweryfikować pliku obrazu: Nie znaleziono pliku tymczasowego", + "invalid_bcc_map_type": "Nieprawidłowy typ mapy BCC", + "invalid_destination": "Format docelowy jest nieprawidłowy", + "invalid_filter_type": "Nieprawidłowy typ filtra", + "invalid_host": "Nieprawidłowy określony host", + "invalid_mime_type": "Niepoprawny typ mime", + "invalid_nexthop": "Następny format hop jest nieprawidłowy", + "invalid_nexthop_authenticated": "Następny hop już istnieje z innymi danymi logowania, zaktualizuj najpierw istniejące dane uwierzytelniające dla tego hopa.", + "invalid_recipient_map_new": "Określony nieprawidłowy nowy odbiorca", + "invalid_recipient_map_old": "Określony nieprawidłowy pierwotny odbiorca", + "invalid_reset_token": "Nieprawidłowy token resetu", + "ip_list_empty": "Lista dozwolonych adresów IP nie może być pusta", + "mailbox_defquota_exceeds_mailbox_maxquota": "Domyślny limit skrzynki pocztowej przekracza maksymalny dozwolony limit", + "malformed_username": "Nieprawidłowy format nazwy użytkownika", + "map_content_empty": "Zawartość mapy nie może być pusta", + "max_age_invalid": "Nieprawidłowa wartość maksymalnego wieku: %s", + "mode_invalid": "Tryb %s jest nieprawidłowy", + "mx_invalid": "Rekord MX %s jest nieprawidłowy", + "mysql_error": "Błąd MySQL: %s", + "network_host_invalid": "Nieprawidłowa sieć lub host: %s", + "next_hop_interferes": "%s powoduje konflikt z nexthopem %s", + "next_hop_interferes_any": "Istniejący next hop koliduje z %s.”", + "nginx_reload_failed": "Nie udało się przeładować Nginx: %s", + "no_user_defined": "Brak zdefiniowanego użytkownika", + "password_reset_invalid_user": "Skrzynka pocztowa nie została znaleziona lub nie ustawiono adresu e-mail do odzyskiwania", + "password_reset_na": "Odzyskiwanie hasła jest obecnie niedostępne. Skontaktuj się ze swoim administratorem.", + "private_key_error": "Błąd klucza prywatnego: %s", + "pushover_credentials_missing": "Brak tokena i/lub klucza Pushover", + "pushover_key": "Klucz Pushover ma niewłaściwy format", + "pushover_token": "Token Pushover ma zły format", + "recipient_map_entry_exists": "Istnieje wpis mapy odbiorcy \"%s", + "recovery_email_failed": "Nie można wysłać e-maila odzyskiwania. Skontaktuj się ze swoim administratorem.", + "redis_error": "Błąd Redis: %s", + "relayhost_invalid": "Wpis mapy %s jest niepoprawny", + "release_send_failed": "Nie udało się zwolnić wiadomości: %s", + "required_data_missing": "Brakuje wymaganych danych %s", + "reset_f2b_regex": "Filtr Regex nie mógł zostać zresetowany na czas, spróbuj ponownie lub poczekaj jeszcze kilka sekund i przeładuj stronę.", + "reset_token_limit_exceeded": "Limit tokenów Reset został przekroczony. Spróbuj ponownie później.", + "rl_timeframe": "Nieprawidłowo ustawiony przedział czasu limitu", + "rspamd_ui_pw_length": "Hasło do Rspamd UI powinno mieć co najmniej 6 znaków długości", + "script_empty": "Skrypt nie może być pusty", + "set_acl_failed": "Nie udało się ustawić ACL", + "settings_map_invalid": "Ustawienia id mapy %s są nieprawidłowe", + "sieve_error": "Błąd podczas analizy skryptu Sieve: %s", + "spam_learn_error": "Błąd uczenia spamu: %s", + "subject_empty": "Temat nie może być pusty", + "targetd_relay_domain": "Domena docelowa %s jest skonfigurowana jako domena przekazująca relay", + "template_exists": "Szablon %s już istnieje", + "template_id_invalid": "Identyfikator szablonu %s niepoprawny", + "template_name_invalid": "Nazwa szablonu niepoprawna", + "temp_error": "Tymczasowy błąd", + "text_empty": "Pole tekstowe nie może być puste", + "tfa_token_invalid": "Niepoprawny token TFA", + "tls_policy_map_dest_invalid": "Podano błędne lub nieobsługiwane miejsce docelowe dla tej polityki", + "tls_policy_map_entry_exists": "Istnieje mapa polityki TLS \"%s", + "tls_policy_map_parameter_invalid": "Parametr polityki jest nieprawidłowy", + "totp_verification_failed": "Weryfikacja TOTP nie powiodła się", + "transport_dest_exists": "Miejsce docelowe transportu „%s” już istnieje", + "webauthn_verification_failed": "Weryfikacja WebAuthn nie powiodła się: %s", + "webauthn_authenticator_failed": "Wybrany autoryzator nie został odnaleziony", + "webauthn_publickey_failed": "Nie przechowywano klucza publicznego dla wybranego uwierzytelniacza", + "webauthn_username_failed": "Wybrany autoryzator należy do innego konta", + "unknown": "Wystąpił nieznany błąd", + "unknown_tfa_method": "Nieznana metodą TFA", + "unlimited_quota_acl": "Ustawienie nieograniczonego limitu przestrzeni jest zabronione przez reguły ACL", + "value_missing": "Proszę o podanie wszystkich wartości", + "version_invalid": "Wersja %s jest niepoprawna", + "yotp_verification_failed": "Weryfikacja OTP Yubico nie powiodła się: %s" }, "edit": { "active": "Aktywny", @@ -178,7 +589,7 @@ "previous": "Poprzednia strona", "quota_mb": "Limit wielkośći (MiB)", "relay_all": "Przekaż wszystkim odbiorcom", - "relay_all_info": "Jeśli decydujesz się nie przekazywać wszystkim odbiorcom, musisz dodać (\"ślepą\")skrzynkę dla każdego poszczególnego odbiorcy, któremu należy przekazać.", + "relay_all_info": "↪ Jeśli zdecydujesz się nie przekazywać wszystkim odbiorcom, musisz dodać (\"ślepą\")skrzynkę dla każdego poszczególnego odbiorcy, któremu należy przekazać.", "relay_domain": "Domena przekaźnikowa", "remove": "Usuń", "resource": "Zasób", @@ -190,15 +601,104 @@ "target_domain": "Domena docelowa", "title": "Edytuj obiekt", "unchanged_if_empty": "Jeżli bez zmian, nie wypełniaj", - "username": "Nazwa użytkownika" + "username": "Nazwa użytkownika", + "delete_ays": "Proszę o potwierdzenie procesu usuwania.", + "acl": "ACL (Pozwolenie)", + "admin": "Edytyj administratora", + "advanced_settings": "Ustawienia zaawansowane", + "allow_from_smtp": "Zezwalaj tylko tym adresom IP na używanie SMTP", + "allow_from_smtp_info": "Pozostaw puste, aby zezwolić na wszystkich nadawców.
    adresy IPv4/IPv6 i sieci.", + "allowed_protocols": "Dozwolone protokoły dla bezpośredniego dostępu użytkownika (nie wpływa na protokoły haseł aplikacji)", + "app_name": "Nazwa aplikacji", + "app_passwd": "Hasło do aplikacji", + "app_passwd_protocols": "Dozwolone protokoły dla hasła aplikacji", + "automap": "Spróbuj automatycznie mapować foldery (np. „Sent items”, „Sent” ⇒ „Sent” itp.)", + "bcc_dest_format": "Miejscem docelowym BCC musi być jeden prawidłowy adres e-mail.
    Jeśli chcesz wysłać kopię na wiele adresów, utwórz alias i użyj go tutaj.", + "client_id": "Id klienta", + "client_secret": "Tajny klucz klienta(sekret)", + "comment_info": "Komentarz prywatny nie jest widoczny dla użytkownika, natomiast komentarz publiczny jest wyświetlany jako podpowiedź po najechaniu kursorem w widoku użytkownika", + "created_on": "Stworzony na", + "custom_attributes": "Niestandardowe atrybuty", + "delete2": "Usuń wiadomości na koncie docelowym, które nie występują na koncie źródłowym", + "disable_login": "Zablokuj logowanie (przychodząca poczta nadal będzie przyjmowana)", + "domain_footer": "Stopka dla całej domeny", + "domain_footer_html": "Stopka HTML", + "domain_footer_info": "Stopki dla całej domeny są dodawane do wszystkich wychodzących wiadomości e-mail powiązanych z adresami w tej domenie.
    W stopce można użyć następujących zmiennych:", + "domain_footer_info_vars": { + "auth_user": "{= auth_user =} - Uwierzytelniona nazwa użytkownika określona przez MTA", + "from_user": "{= from_user =} – część użytkownika (local-part) adresu nadawcy; np. dla „moo@mailcow.tld\n” zwróci „moo”", + "from_name": "{= from_name =} – nazwa nadawcy (From name) z nagłówka wiadomości; np. dla \"Mailcow <moo@mailcow.tld>\" zwróci \"Mailcow\"", + "from_addr": "{= from_addr =} – pełny adres nadawcy (z części envelope)", + "from_domain": "{= from_domain =} – część domenowa adresu nadawcy (z envelope)", + "custom": "{= foo =} - jeśli skrzynka pocztowa ma niestandardowy atrybut „foo” o wartości „bar”, zwraca „bar”" + }, + "domain_footer_plain": "Stopka w formacie tekstowym(PLAIN)", + "domain_footer_skip_replies": "Nie dodawaj stopki do odpowiedzi na e-maile", + "extended_sender_acl": "Adresy nadawców zewnętrznych", + "extended_sender_acl_info": "Klucz DKIM dla domeny powinien zostać zaimportowany, jeśli jest dostępny.\nPamiętaj, aby dodać ten serwer do odpowiadającego rekordu SPF typu TXT.\nKiedy domena lub domena aliasu zostanie dodana do tego serwera i pokrywa się z zewnętrznym adresem, zewnętrzny adres zostanie usunięty.", + "force_pw_update": "Wymuszenie aktualizacji hasła przy następnym logowaniu", + "force_pw_update_info": "Ten użytkownik będzie mógł logować się wyłącznie tutaj %s. Hasła aplikacyjne pozostają aktywne.", + "footer_exclude": "Wyklucz ze stopki", + "gal": "Globalna lista adresowa", + "gal_info": "GAL zawiera wszystkie obiekty domeny i nie może być edytowana przez żadnego użytkownika. Informacje o dostępności (free/busy) w SOGo są niedostępne, jeśli funkcja jest wyłączona!Uruchom ponownie SOGo, aby zastosować zmiany.", + "generate": "generuj", + "grant_types": "Rodzaje grantów(Typy przyznawania dostępu)", + "internal": "Wewnętrzny", + "internal_info": "Aliasów wewnętrznych można używać tylko w obrębie własnej domeny lub domen aliasów.", + "last_modified": "Ostatnio modyfikowany", + "lookup_mx": "Destination to wyrażenie regularne dopasowujące nazwę serwera MX (np. .*.google.com — aby kierować całą pocztę wysyłaną do MX kończących się na google.com przez ten hop)", + "pushover_title": "Tytuł powiadomienia", + "pushover_sound": "Dźwięk powiadomienia", + "pushover_vars": "Gdy nie zdefiniowano żadnego filtra nadawcy, brane będą pod uwagę wszystkie wiadomości. Filtry oparte na wyrażeniach regularnych (regex) oraz dokładne dopasowania nadawców można definiować indywidualnie — będą one przetwarzane kolejno, niezależnie od siebie. Dostępne zmienne do użycia w treści i tytule (należy pamiętać o zasadach ochrony danych osobowych).", + "pushover_verify": "Zweryfikuj dane logowania", + "quota_warning_bcc": "Ukryta kopia ostrzeżenia o przekroczeniu limitu", + "quota_warning_bcc_info": "Ostrzeżenia będą wysyłane jako osobne kopie do poniższych odbiorców. Temat wiadomości zostanie rozszerzony o nazwę użytkownika w nawiasach, na przykład: Ostrzeżenie o limicie (user@example.com\n).", + "ratelimit": "Limit wysyłania", + "redirect_uri": "Adres przekierowania / adres zwrotny (Redirect/Callback URL)", + "relay_transport_info": "
    Informacja
    **Możesz zdefiniować mapy transportu (transport maps) dla niestandardowego miejsca docelowego tej domeny.** Jeśli nie zostanie to ustawione, zostanie wykonane wyszukiwanie", + "relay_unknown_only": "Przekazuj (relay) tylko nieistniejące skrzynki pocztowe. Istniejące skrzynki będą dostarczane lokalnie.", + "relayhost": "Transporty przypisane do nadawcy", + "scope": "Zakres", + "sender_acl_disabled": "Sprawdzanie nadawcy jest wyłączone", + "sender_acl_info": "Jeżeli użytkownik skrzynki pocztowej A ma pozwolenie na wysyłkę jako użytkownik skrzynki B, adres nadawcy nie jest automatycznie wyświetlany jako opcja w polu „Od” w SOGo.
    \nUżytkownik skrzynki B musi utworzyć delegację w SOGo, aby użytkownik A mógł wybrać ich adres jako nadawcę. Aby zdelegować skrzynkę w SOGo, użyj menu (trzy kropki) po prawej stronie nazwy skrzynki w lewym górnym rogu, będąc w widoku poczty. To zachowanie nie dotyczy adresów aliasów.", + "sieve_desc": "Krótki opis", + "sieve_type": "Typ filtra", + "skipcrossduplicates": "Pomijaj duplikaty wiadomości w różnych folderach (pierwsza napotkana wiadomość zostaje zachowana).", + "sogo_access": "Bezpośrednie przekazywanie do SOGo", + "sogo_access_info": "Po zalogowaniu użytkownik jest automatycznie przekierowywany do SOGo.", + "sogo_visible": "Alias jest widoczny w SOGO", + "sogo_visible_info": "Ta opcja dotyczy tylko obiektów, które mogą być wyświetlane w SOGo (aliasy współdzielone lub nie współdzielone, wskazujące przynajmniej na jedną lokalną skrzynkę).\nJeśli obiekt zostanie ukryty, alias nie będzie dostępny jako wybieralny nadawca w SOGo.", + "spam_alias": "Tworzenie lub zmiana ograniczonych czasowo adresów aliasów", + "spam_filter": "Filtr spam", + "spam_policy": "Dodaj lub usuń elementy z listy dozwolonych / zablokowanych", + "spam_score": "Ustaw własny poziom punktacji spamu", + "timeout1": "Limit czasu połączenia z serwerem zdalnym", + "timeout2": "Limit czasu połączenia z serwerem lokalnym", + "validate_save": "Zatwierdź i zapisz", + "pushover_info": "Ustawienia powiadomień push będą miały zastosowanie do wszystkich czystych (niespamowych) wiadomości dostarczanych do %s, w tym aliasów (współdzielonych, niewspółdzielonych, oznaczonych)", + "mailbox_quota_def": "Domyślny limit skrzynki pocztowej", + "mailbox_relayhost_info": "Dotyczy wyłącznie skrzynki pocztowej i bezpośrednich aliasów, nadpisuje ustawienie serwera pośredniczącego (relayhost) dla domeny.", + "maxbytespersecond": "Max. Ilość bajtów na sekundę
    (0 = unlimited)", + "mailbox_rename": "Zmień nazwę skrzynki pocztowej", + "mailbox_rename_agree": "Stworzyłem kopię zapasową.", + "mailbox_rename_warning": "WAŻNE! Utwórz kopię zapasową przed zmianą nazwy skrzynki pocztowej.", + "mailbox_rename_alias": "Tworzenie aliasów automatycznie", + "mailbox_rename_title": "Nowa nazwa lokalnej skrzynki pocztowej" }, "footer": { "cancel": "Anuluj", "confirm_delete": "Potwierdź usunięcie", "delete_now": "Usuń teraz", - "delete_these_items": "Czy jesteś pewien, że chcesz usunąć następujące elementy?", + "delete_these_items": "Proszę potwierdzić zmiany w poniższym identyfikatorze obiektu", "loading": "Proszę czekać...", - "restart_now": "Uruchom ponownie teraz" + "restart_now": "Uruchom ponownie teraz", + "hibp_check": "Sprawdź w stosunku do haveibeenpwned.com", + "hibp_nok": "Dopasowano! To potencjalnie niebezpieczne hasło!", + "hibp_ok": "Nie znaleziono żadnego dopasowania.", + "nothing_selected": "Nic wybranego", + "restart_container": "Zresetuj kontener", + "restart_container_info": "Ważne: Łagodne ponowne uruchomienie może zająć trochę czasu — proszę poczekać na jego zakończenie.", + "restarting_container": "Ponowne uruchomienie kontenera, może to zająć trochę czasu" }, "header": { "administration": "Administrowanie", @@ -206,16 +706,40 @@ "mailcow_config": "Konfiguracja", "quarantine": "Kwarantanna", "restart_sogo": "Uruchom ponownie SOGo", - "user_settings": "Ustawienia użytkownika" + "user_settings": "Ustawienia użytkownika", + "apps": "Aplikacje", + "debug": "Informacja", + "mailcow_system": "System", + "restart_netfilter": "Uruchom ponownie netfilter" }, "info": { - "no_action": "Żadne działanie nie ma zastosowania" + "no_action": "Żadne działanie nie ma zastosowania", + "awaiting_tfa_confirmation": "Oczekiwanie na potwierdzenie TFA", + "session_expires": "Twoja sesja wygaśnie za około 15 sekund" }, "login": { "delayed": "Logowanie zostało opóźnione o %s sekund.", "login": "Zaloguj się", "password": "Hasło", - "username": "Nazwa użytkownika" + "username": "Nazwa użytkownika", + "forgot_password": "Zapomniałeś hasła?", + "login_linkstext": "Login nieprawidłowy?", + "login_usertext": "Zaloguj się jako użytkownik", + "login_domainadmintext": "Zaloguj się jako administrator domeny", + "login_admintext": "Zaloguj się jako admin", + "other_logins": "lub zaloguj za pomocą", + "email": "Adres e-mail", + "back_to_mailcow": "Wróć do mailcow", + "fido2_webauthn": "Logowanie FIDO2/WebAuthn", + "invalid_pass_reset_token": "Token resetowania hasła jest nieprawidłowy lub wygasł.
    Proszę poprosić o nowy link do resetowania hasła.", + "login_user": "Logowanie użytkownika", + "login_dadmin": "Logowanie administratora domeny", + "login_admin": "Login Administratora", + "mobileconfig_info": "Zaloguj się jako użytkownik skrzynki pocztowej, aby pobrać żądany profil połączenia Apple.", + "new_password": "Nowe hasło", + "new_password_confirm": "Potwierdź nowe hasło", + "reset_password": "Zresetuj hasło", + "request_reset_password": "Poproś o zmianę hasła" }, "mailbox": { "action": "Działanie", @@ -275,7 +799,48 @@ "tls_enforce_out": "Uruchom TLS wychodzące", "toggle_all": "Zaznacz wszystkie", "username": "Nazwa użytkownika", - "weekly": "Co tydzień" + "weekly": "Co tydzień", + "add_alias_expand": "Rozszerz alias na domeny alias", + "add_bcc_entry": "Dodaj mapę BCC", + "add_filter": "Dodaj filtr", + "recipient_map_old_info": "Mapa odbiorcy (pierwotne miejsce docelowe) musi być prawidłowym adresem e-mail lub nazwą domeny.", + "recipient_maps": "Mapy odbiorców", + "relay_unknown": "Przekazuj nieznane skrzynki pocztowe", + "running": "Uruchomione", + "sender": "Nadawca", + "set_postfilter": "Oznacz jako postfilter", + "set_prefilter": "Oznacz jako prefilter", + "sieve_info": "Możesz przechowywać wiele filtrów dla każdego użytkownika, jednak w danym momencie aktywny może być tylko jeden prefilter i jeden postfilter
    \nKażdy filtr będzie przetwarzany w opisanej kolejności. Ani błędny skrypt, ani polecenie „keep;” nie zatrzymają przetwarzania kolejnych skryptów.\nZmiany w globalnych skryptach Sieve spowodują ponowne uruchomienie usługi Dovecot.

    \nGlobalny prefiltr Sieve • Prefilter • Skrypty użytkownika • Postfilter • Globalny postfilter Sieve", + "sieve_preset_1": "Odrzuć pocztę z prawdopodobnymi niebezpiecznymi typami plików", + "sieve_preset_2": "Zawsze zaznaczaj e-mail konkretnego nadawcy jako odczytane", + "sieve_preset_3": "Odrzuć po cichu, zatrzymaj cały proces sieve", + "sieve_preset_4": "Plik do INBOX, pomiń dalszy proces przez filtry sieve", + "sieve_preset_5": "Automatyczna odpowiedź (urlopowa)", + "sieve_preset_6": "Odrzuć pocztę z odpowiedzią", + "sieve_preset_7": "Przekierowanie wiadomości z możliwością zachowania lub usunięcia kopii", + "sieve_preset_8": "Przekieruj wiadomość e-mail od określonego nadawcy, oznacz jako odczytaną i posortuj do podfoldera", + "sieve_preset_header": "Zbacz przykładowe ustawienia poniżej. Więcej szczegółów znajdziesz w Wikipedii.", + "sogo_visible": "Alias jest widoczny w SOGo", + "sogo_visible_n": "Ukryj alias w SOGo", + "sogo_visible_y": "Pokaż alias w SOGo", + "stats": "Statystyki", + "status": "Status", + "syncjob_check_log": "Sprawdź log", + "syncjob_last_run_result": "Wynik ostatniego uruchomienia", + "syncjob_EX_OK": "Sukces", + "syncjob_EXIT_CONNECTION_FAILURE": "Problem z połączeniem", + "syncjob_EXIT_TLS_FAILURE": "Problem z szyfrowanym połączeniem", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problem uwierzytelniania", + "syncjob_EXIT_OVERQUOTA": "Docelowa skrzynka pocztowa przekroczyła limit pojemności", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nie można połączyć się ze zdalnym serwerem", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Niewłaściwa nazwa użytkownika lub hasło", + "table_size": "Rozmiar tabeli", + "table_size_show_n": "Pokaż %s elementy", + "templates": "Szablony", + "template": "Szablon", + "tls_map_dest": "Miejsce docelowe", + "tls_map_dest_info": "Przykłady: example.org, .example.org, [mail.example.org]:25", + "tls_map_parameters": "Parametry" }, "quarantine": { "action": "Działanie", @@ -283,10 +848,40 @@ "quarantine": "Kwarantanna", "quick_actions": "Szybkie działania", "remove": "Usuń", - "toggle_all": "Zaznacz wszystkie" + "toggle_all": "Zaznacz wszystkie", + "confirm_delete": "Potwierdź usunięcie tego elementu.", + "learn_spam_delete": "Zapamiętaj jako spam i usuwaj w przyszłości", + "quick_delete_link": "Otwórz szybki link do usuwania", + "refresh": "Odśwież", + "rejected": "Odrzucony", + "release": "Zwolnij", + "release_body": "Dołączyliśmy Twoją wiadomość jako plik eml do tej wiadomości.", + "release_subject": "Potencjalnie szkodliwa pozycja kwarantanny %s", + "show_item": "Pokaż element", + "spam": "Spam", + "spam_score": "Wskaźnik", + "subj": "Temat", + "table_size": "Rozmiar tabeli", + "table_size_show_n": "Pokaż %s elementy", + "text_from_html_content": "Zawartość (przekonwertowany HTML)", + "text_plain_content": "Zawartość (tekst zwykły)", + "type": "Typ" }, "queue": { - "queue_manager": "Queue Manager" + "queue_manager": "Menedżer kolejki", + "delete": "Usuń wszystko", + "ays": "Potwierdź, że chcesz usunąć wszystkie elementy z bieżącej kolejki.", + "flush": "Opróżnij kolejkę wiadomości", + "info": "Kolejka poczty zawiera wszystkie wiadomości e-mail oczekujące na dostarczenie.\nJeśli wiadomość e-mail pozostaje w kolejce przez dłuższy czas, system automatycznie ją usuwa.
    \nKomunikat błędu dla danej wiadomości zawiera informacje o przyczynie, dla której nie mogła zostać dostarczona.", + "legend": "Funkcje zarządzania kolejką poczty:", + "deliver_mail": "Dostarcz", + "deliver_mail_legend": "Próby ponownego dostarczenia wybranych wiadomości.", + "hold_mail": "Wstrzymane", + "hold_mail_legend": "Wstrzymuję wybrane maile. (Zapobiega dalszym próbom dostarczenia)", + "show_message": "Pokaż wiadomość", + "unban": "Usuń zablokowanie w kolejce", + "unhold_mail": "Zwolnij wiadomość", + "unhold_mail_legend": "Zwalnia wybrane wiadomości do dostarczenia (wymaga wcześniejszego wstrzymania)." }, "start": { "help": "Pokaż/Ukryj panel pomocy", @@ -297,7 +892,7 @@ "alias_added": "Alias/y został/y dodany/e", "alias_domain_removed": "Usunięto alias domeny %s", "alias_modified": "Zapisano zmiany w aliasie/ach %s", - "alias_removed": "Usunięto alias %s ", + "alias_removed": "Usunięto alias %s", "aliasd_added": "Dodano alias domeny %s", "aliasd_modified": "Zapisano zmiany w aliasie domeny %s", "dkim_added": "Klucz DKIM został zapisany", @@ -312,24 +907,85 @@ "f2b_modified": "Zmiany w Fail2ban zostały zapisane", "forwarding_host_added": "Dodano hosta przekazującego %s", "forwarding_host_removed": "Usunięto hosta przekazującego %s", - "item_deleted": "", - "items_deleted": "Item %s successfully deleted", + "item_deleted": "Element %s skutecznie usunięte", + "items_deleted": "Elementy %s skutecznie usunięte", "mailbox_added": "Dodano skrzynkę %s", "mailbox_modified": "Zapisano zmiany w skrzynce %s", "mailbox_removed": "Usunięto skrzynkę %s", "object_modified": "Zapisano zmiany w obiekcie %s", "resource_added": "Dodano śródło %s", "resource_modified": "Zapisano zmiany w skrzynce %s", - "resource_removed": "Usunięto zasób %s" + "resource_removed": "Usunięto zasób %s", + "template_removed": "Szablon o identyfikatorze %s został usunięty.", + "tls_policy_map_entry_deleted": "Mapa polityki TLS o identyfikatorze %s została usunięta", + "tls_policy_map_entry_saved": "Wpis mapy polityki TLS \"%s\" został zapisany", + "ui_texts": "Zapisane zmiany w tekstach UI", + "upload_success": "Plik przesłany pomyślnie", + "verified_fido2_login": "Zweryfikowany login FIDO2", + "verified_totp_login": "Zweryfikowany login TOTP", + "verified_webauthn_login": "Zweryfikowany login WebAuthn", + "verified_yotp_login": "Zweryfikowany login Yubico OTP", + "acl_saved": "ACL dla obiektu %s zapisany", + "admin_added": "Administrator %s został dodany", + "admin_api_modified": "Zmiany w API zostały zapisane", + "admin_removed": "Administrator %s został usunięty", + "app_links": "Zapisane zmiany w linkach aplikacji", + "app_passwd_added": "Dodano nowe hasło aplikacji", + "app_passwd_removed": "Usunięto ID hasła aplikacji %s", + "bcc_deleted": "Wpisy map BCC usunięte: %s", + "bcc_edited": "Wpis mapy BCC %s edytowany", + "bcc_saved": "Wpis mapy BCC zapisany", + "cors_headers_edited": "Ustawienia CORS zostały zapisane", + "custom_login_modified": "Dostosowanie logowania zostało pomyślnie zapisane", + "db_init_complete": "Inicjalizacja bazy danych zakończona", + "delete_filter": "Filtry %s ID usunięte", + "delete_filters": "Usunięte filtry: %s", + "deleted_syncjob": "Usunięte syncjob ID %s", + "deleted_syncjobs": "Usunięte syncjobs: %s”.", + "domain_add_dkim_available": "Klucz DKIM już istniał", + "dkim_duplicated": "Klucz DKIM dla domeny %s został skopiowany do %s", + "domain_footer_modified": "Zmiany w stopce domeny %s zostały zapisane", + "dovecot_restart_success": "Dovecot został pomyślnie zrestartowany", + "f2b_banlist_refreshed": "ID listy banów został pomyślnie odświeżony.", + "global_filter_written": "Filtr został pomyślnie zapisany do pliku", + "hash_deleted": "Hash usunięty", + "iam_test_connection": "Połączenie powiodło się", + "ip_check_opt_in_modified": "Sprawdzenie adresu IP zostało pomyślnie zapisane", + "item_released": "Pozycja %s zwolniona", + "items_released": "Wybrane elementy zostały zwolnione", + "learned_ham": "Pomyślnie nauczono ID %s jako ham", + "license_modified": "Zmiany w licencji zostały zapisane", + "logged_in_as": "Zalogowany jako %s", + "mailbox_renamed": "Nazwa skrzynki pocztowej została zmieniona z %s na %s", + "nginx_reloaded": "Nginx został przeładowany", + "password_policy_saved": "Polityka haseł została pomyślnie zapisana", + "password_changed_success": "Hasło zostało pomyślnie zmienione", + "pushover_settings_edited": "Ustawienia Pushover pomyślnie ustawione, proszę zweryfikować dane uwierzytelniające.", + "qlearn_spam": "Identyfikator wiadomości %s został nauczony jako spam i usunięty", + "queue_command_success": "Polecenie kolejki zostało pomyślnie wykonane", + "recipient_map_entry_deleted": "Id mapy odbiorcy %s został usunięty", + "recipient_map_entry_saved": "Wpis mapy odbiorcy \"%s\" został zapisany", + "recovery_email_sent": "E-mail do odzyskiwania wysłany do %s", + "relayhost_added": "Wpis mapy %s został dodany", + "relayhost_removed": "Wpis mapy %s został usunięty", + "reset_main_logo": "Reset do domyślnego logo", + "rl_saved": "Limit szybkości dla obiektu %s został zapisany", + "rspamd_ui_pw_set": "Hasło do Rspamd UI pomyślnie ustawione", + "saved_settings": "Zapisane ustawienia", + "settings_map_added": "Dodano ustawienia (wpis mapy)", + "settings_map_removed": "Usunięte ustawienia mapy ID %s", + "sogo_profile_reset": "Profil SOGo dla użytkownika/ów został zresetowany", + "template_added": "Dodano szablon", + "template_modified": "Zmiany w szablonie/ach zostały zapisane" }, "tfa": { - "api_register": "%s używa Yubico Cloud API. Proszę pobrać klucz API dla Twojego klucza here", + "api_register": "%s używa Yubico Cloud API. Proszę pobrać klucz API dla Twojego klucza tutaj", "confirm": "Potwierdź", "confirm_totp_token": "Potwierdź zmiany przez wprowadzenie wygenerowanego tokenu", "delete_tfa": "Wyłącz TFA", "disable_tfa": "Wyłącz TFA do kolejnego udanego logowania", "enter_qr_code": "Twój kod TOTP, jeśli Twoje urządzenie nie skanuje kodów QR.", - "key_id": "Identyfikator dla Twojego YubiKey", + "key_id": "Identyfikator dla twojego urządzenia", "key_id_totp": "Identyfikator dla Twojego klucza", "none": "Deaktywuj", "scan_qr_code": "Zeskanuj następujący kod aplikacją uwierzytelniającą lub wprowadź kod ręcznie.", @@ -339,8 +995,16 @@ "totp": "Time-based OTP (Google Authenticator itd.)", "webauthn": "Uwierzytelnianie WebAuthn", "waiting_usb_auth": "Czekam na urządzenie USB...

    Wciśnij teraz przycisk na urządzeniu WebAuthn USB.", - "waiting_usb_register": " Czekam na urządzenie USB...

    Wprowadź swoje hasło powyżej i potwierdź rejestrację WebAuthn przez naciśnięcie przycisku na urządzeniu WebAuthn USB.", - "yubi_otp": "Uwierzytelnianie Yubico OTP" + "waiting_usb_register": " Czekam na urządzenie USB...

    Wprowadź swoje hasło powyżej i potwierdź rejestrację przez naciśnięcie przycisku na urządzeniu USB.", + "yubi_otp": "Uwierzytelnianie Yubico OTP", + "authenticators": "Uwierzytelniacze", + "error_code": "Kod błędu", + "init_webauthn": "Inicjalizacja, proszę czekać...", + "reload_retry": "- (przeładuj przeglądarkę, jeśli błąd nadal występuje)", + "start_webauthn_validation": "Rozpocznij walidację", + "tfa_token_invalid": "Token TFA nieprawidłowy", + "u2f_deprecated": "Wygląda na to, że Twój klucz został zarejestrowany przy użyciu przestarzałej metody U2F. Dezaktywujemy dla Ciebie uwierzytelnianie dwuskładnikowe i usuniemy Twój klucz.", + "u2f_deprecated_important": "Zarejestruj swój klucz w panelu administracyjnym za pomocą nowej metody WebAuthn." }, "user": { "action": "Działanie", @@ -370,7 +1034,7 @@ "edit": "Edytuj", "encryption": "Szyfrowanie", "excludes": "Wyłączenia", - "force_pw_update": "Musisz zmienić hasło, aby używać webmaila.", + "force_pw_update": "Musisz ustawić nowe hasło, aby mieć dostęp do usług groupware.", "hour": "Godzina", "hourly": "Co godzinę", "hours": "Godziny", @@ -378,7 +1042,7 @@ "interval": "Zakres", "is_catch_all": "Funkcja catch-all dla domen/y", "last_run": "Ostatnie uruchomienie", - "mailbox_details": " Szczegóły skrzynki", + "mailbox_details": "Szczegóły", "messages": "wiadomości", "never": "Nigdy", "new_password": "Nowe hasło", @@ -390,17 +1054,17 @@ "remove": "Usuń", "save_changes": "Zapisz zmiany", "shared_aliases": "Aliasy współdzielone", - "shared_aliases_desc": "Na aliasy współdzielone nie wpływają filtry spamu i ustawienia TLS.", - "show_sieve_filters": "Twój filtr sieve", - "sogo_profile_reset": "Usuń profil SOGo (webmail)", - "sogo_profile_reset_help": "To usunie ustawienia SOGo bezpowrotnie.", - "sogo_profile_reset_now": "Usuń profil teraz", - "spam_aliases": "Tymczasowy alias email", + "shared_aliases_desc": "Współdzielone aliasy nie są objęte ustawieniami specyficznymi dla użytkownika, takimi jak filtr antyspamowy czy polityka szyfrowania.\nOdpowiadające im filtry antyspamowe mogą być tworzone wyłącznie przez administratora — jako polityki obowiązujące dla całej domeny.", + "show_sieve_filters": "Pokaż filtr sieve aktywnego użytkownika", + "sogo_profile_reset": "Zresetuj profil SOGo", + "sogo_profile_reset_help": "Spowoduje to usunięcie profilu użytkownika SOGo oraz bezpowrotne usunięcie wszystkich danych kontaktów i kalendarza.", + "sogo_profile_reset_now": "Zresetuj profil teraz", + "spam_aliases": "Tymczasowe aliasy email", "spamfilter": "Filtr spamu", - "spamfilter_behavior": "Rating", + "spamfilter_behavior": "Ocena", "spamfilter_bl": "Czarna lista", - "spamfilter_bl_desc": "Adresy email z czarnej listy zawsze klasyfikuj jako spam i odrzucaj. Można użyć wildcards.", - "spamfilter_default_score": "Wartości domyślne:", + "spamfilter_bl_desc": "Adresy e-mail znajdujące się na liście zablokowanych (denylist) są zawszeklasyfikowane jako spam i odrzucane.\nOdrzucone wiadomości nie sąkopiowane do kwarantanny.\nMożna używać symboli wieloznacznych (wildcardów).\nFiltr jest stosowany wyłącznie do bezpośrednich aliasów (aliasów kierujących do jednej skrzynki pocztowej), z wyłączeniem aliasów typu „catch-all” oraz samej skrzynki.", + "spamfilter_default_score": "Wartości domyślne", "spamfilter_green": "Zielony: ta wiadomość nie jest spamem", "spamfilter_hint": "Pierwsza wartość oznacza \"niską punktację spam\", druga wartość oznacza \"wysoką punktację spam\".", "spamfilter_red": "Czerwony: ta wiadomość jest spamem i zostanie odrzucona przez serwer", @@ -411,13 +1075,13 @@ "spamfilter_table_remove": "Usuń", "spamfilter_table_rule": "Zasada", "spamfilter_wl": "Biała lista", - "spamfilter_wl_desc": "Adresy email z białej listy nigdy nie klasyfikuj jako spam. Można użyć wildcards.", + "spamfilter_wl_desc": "Adresy e-mail znajdujące się na liście dozwolonych (allowlist) są zaprogramowane tak, aby nigdy nie były klasyfikowane jako spam.\nMożna używać symboli wieloznacznych (wildcardów).\nFiltr jest stosowany wyłącznie do bezpośrednich aliasów (aliasów wskazujących na jedną skrzynkę pocztową), z wyłączeniem aliasów typu „catch-all” oraz samej skrzynki pocztowej", "spamfilter_yellow": "Żółty: ta wiadomość może być spamem, zostanie oznaczona jako spam i przeniesiona do folderu spam", - "sync_jobs": "Polecenie synchronizacji", + "sync_jobs": "Zadania synchronizacji", "tag_handling": "Ustaw obsługę znaczników pocztowych", "tag_help_example": "Przykład adresu email z etykietą: ja+Facebook@example.org", "tag_help_explain": "W podfolderze: tworzy nowy podfolder z nazwą taką jak etykieta, który zostanie umieszczony pod Skrzynką odbiorczą (\"Skrzynka odbiorcza/Facebook\").
    \r\nW temacie: nazwy etykiet zostaną dodane na początku tematów wiadomości, np.: \"[Facebook] Moje wiadomości\".", - "tag_in_none": "Nic nie robić", + "tag_in_none": "Nie wykonuj żadnej akcji", "tag_in_subfolder": "W podfolderze", "tag_in_subject": "W temacie", "tls_enforce_in": "Uruchom TLS przychodzące", @@ -428,6 +1092,198 @@ "username": "Nazwa użytkownika", "week": "Tydzień", "weekly": "Co tydzień", - "weeks": "Tygodnie" + "weeks": "Tygodnie", + "q_add_header": "Spam", + "advanced_settings": "Ustawienia zaawansowane", + "app_hint": "Hasła aplikacji są alternatywnymi hasłami dla logowania IMAP, SMTP, CalDAV, CardDAV i EAS. Nazwa użytkownika pozostaje niezmieniona. Webmail SOGo nie jest dostępny za pośrednictwem haseł aplikacji.", + "allowed_protocols": "Dozwolone protokoły", + "app_name": "Nazwa aplikacji", + "app_passwds": "Hasła do aplikacji", + "apple_connection_profile": "Profil połączenia Apple", + "apple_connection_profile_complete": "Ten profil połączenia obejmuje parametry IMAP i SMTP, a także ścieżki CalDAV (kalendarze) i CardDAV (kontakty) dla urządzenia Apple.", + "apple_connection_profile_mailonly": "Ten profil połączenia zawiera parametry konfiguracji IMAP i SMTP dla urządzenia Apple.", + "apple_connection_profile_with_app_password": "Nowe hasło aplikacji jest generowane i dodawane do profilu, dzięki czemu nie trzeba wprowadzać hasła podczas konfigurowania urządzenia. Proszę nie udostępniaj tego pliku, ponieważ zapewnia on pełny dostęp do skrzynki pocztowej.", + "attribute": "Atrybut", + "authentication": "Uwierzytelnianie", + "change_password_hint_app_passwords": "Twoje konto ma %d hasła aplikacji, które nie zostaną zmienione. Aby nimi zarządzać, przejdź do zakładki Hasła aplikacji.", + "clear_recent_successful_connections": "Wyczyść udane połączenia", + "create_app_passwd": "Stwórz hasło do aplikacji", + "created_on": "Stworzony na", + "delete_ays": "Proszę o potwierdzenie procesu usuwania.", + "direct_protocol_access": "Ten użytkownik skrzynki pocztowej ma bezpośredni, zewnętrzny dostęp do następujących protokołów i aplikacji. To ustawienie jest kontrolowane przez administratora. Można tworzyć hasła aplikacji, aby przyznać dostęp do poszczególnych protokołów i aplikacji.Przycisk „Webmail” umożliwia jednokrotne logowanie (SSO) do SOGo i jest zawsze dostępny.", + "email": "Email", + "email_and_dav": "E-maile, kalendarze i kontakty", + "empty": "Brak wyników", + "expire_in": "wygasa w", + "fido2_webauthn": "FIDO2/WebAuthn (standard uwierzytelniania)", + "from": "od", + "generate": "generuj", + "last_mail_login": "Ostatni login na skrzynkę pocztową", + "last_pw_change": "Ostatnia zmiana hasła", + "last_ui_login": "Ostatni login UI", + "loading": "Ładowanie...", + "login_history": "Historia logowania", + "mailbox": "Skrzynka pocztowa", + "mailbox_general": "Ogólne ustawienia skrzynki", + "mailbox_settings": "Ustawienia", + "month": "miesiąc", + "months": "miesiące", + "no_last_login": "Brak ostatnich danych logowania do interfejsu użytkownika", + "open_logs": "Otwórz logi użytkownika", + "open_webmail_sso": "­Webmail", + "overview": "Przegląd", + "password": "Hasło", + "password_repeat": "Hasło (powtórz)", + "password_reset_info": "Jeśli nie ma wiadomości e-mail do odzyskiwania hasła, ta funkcja nie może być używana.", + "protocols": "Protokoły", + "pushover_evaluate_x_prio": "Eskaluj wiadomości o wysokim priorytecie [X-Priority: 1]", + "pushover_info": "Ustawienia powiadomień push będą stosowane do wszystkich czystych (niebędących spamem) wiadomości dostarczonych do %s, w tym aliasów (współdzielonych, niewspółdzielonych i oznaczonych).", + "pushover_only_x_prio": "Uwzględniaj tylko wiadomości o wysokim priorytecie [X-Priority: 1]", + "pushover_sender_array": "Uwzględnij następujące adresy e-mail nadawców (oddzielone przecinkami)", + "pushover_sender_regex": "Dopasuj nadawców według następującego regexu", + "pushover_text": "Tekst powiadomienia", + "pushover_title": "Tytuł powiadomienia", + "pushover_sound": "Dźwięk powiadomienia", + "pushover_vars": "Jeśli nie zdefiniowano filtra nadawcy, wszystkie wiadomości będą brane pod uwagę.
    Filtry regex oraz dokładne sprawdzanie nadawców można definiować indywidualnie – są one przetwarzane kolejno i nie zależą od siebie.
    Dostępne zmienne dla treści i tytułu (prosimy pamiętać o zasadach ochrony danych osobowych).", + "pushover_verify": "Zweryfikuj dane logowania", + "pw_recovery_email": "E-mail do odzyskiwania hasła", + "q_all": "Wszystkie kategorie", + "q_reject": "Odrzucono", + "quarantine_category": "Kategoria powiadomień o kwarantannie", + "quarantine_category_info": "Kategoria powiadomień „Odrzucone” obejmuje wiadomości, które zostały odrzucone, natomiast „Folder spam” powiadamia użytkownika o wiadomościach umieszczonych w folderze spam.", + "quarantine_notification_info": "Po wysłaniu powiadomienia elementy zostaną oznaczone jako „powiadomione” i żadne kolejne powiadomienia nie zostaną wysłane dla danego elementu.", + "recent_successful_connections": "Zarejestrowano udane połączenia", + "running": "Uruchomiony", + "save": "Zapisz zmiany", + "sender_acl_disabled": "Sprawdzenie nadawcy jest wyłączone", + "spam_score_reset": "Przywróć domyślne ustawienia serwera", + "status": "Status", + "syncjob_check_log": "Sprawdź log", + "syncjob_last_run_result": "Wynik ostatniego uruchomienia", + "syncjob_EX_OK": "Sukces", + "syncjob_EXIT_CONNECTION_FAILURE": "Problem z połączeniem", + "syncjob_EXIT_TLS_FAILURE": "Problem z szyfrowanym połączeniem", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Problem uwierzytelniania", + "syncjob_EXIT_OVERQUOTA": "Docelowa skrzynka pocztowa przekroczyła limit pojemności", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Nie można połączyć się ze zdalnym serwerem", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Niewłaściwa nazwa użytkownika lub hasło", + "text": "Tekst", + "tfa_info": "Uwierzytelnianie dwuskładnikowe pomaga chronić Twoje konto. Jeśli je włączysz, będziesz potrzebować haseł aplikacji, aby logować się do programów lub usług, które nie obsługują uwierzytelniania dwuskładnikowego (np. klientów poczty).", + "title": "Tytuł", + "value": "Wartość", + "verify": "Zweryfikuj", + "waiting": "Oczekuje", + "with_app_password": "z hasłem aplikacji", + "year": "rok", + "years": "lata" + }, + "warning": { + "session_ua": "Nieprawidłowy token formularza: Błąd walidacji User-Agent", + "cannot_delete_self": "Nie można usunąć zalogowanego użytkownika", + "domain_added_sogo_failed": "Dodano domenę, ale nie udało się ponownie uruchomić SOGo, sprawdź logi serwera.", + "dovecot_restart_failed": "Nie udało się ponownie uruchomić Dovecota, sprawdź logi", + "fuzzy_learn_error": "Błąd uczenia fuzzy hash: %s", + "hash_not_found": "Hash nie został odnaleziony lub został już usunięty", + "ip_invalid": "Pominięto nieprawidłowe IP: %s", + "is_not_primary_alias": "Pominięto alias niebędący głównym: %s", + "no_active_admin": "Nie można dezaktywować ostatniego aktywnego administratora", + "quota_exceeded_scope": "Przekroczono limit pojemności domeny: w tym zakresie domeny można tworzyć tylko skrzynki o nieograniczonej pojemności.", + "session_token": "Nieprawidłowy token formularza: niedopasowanie tokenów" + }, + "datatables": { + "collapse_all": "Zwiń wszystko", + "decimal": ".", + "emptyTable": "Brak danych w tabeli", + "expand_all": "Rozszerz wszystko", + "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": ",", + "lengthMenu": "Pokaż wpisy _MENU_", + "loadingRecords": "Ładowanie...", + "processing": "Proszę czekać...", + "search": "Szukaj:", + "zeroRecords": "Nie znaleziono pasujących rekordów", + "paginate": { + "first": "Pierwszy", + "last": "Ostatni", + "next": "Następny", + "previous": "Poprzedni" + }, + "aria": { + "sortAscending": "Aktywuj, aby posortować kolumnę rosnąco", + "sortDescending": "Aktywuj, aby posortować kolumnę malejąco" + } + }, + "debug": { + "architecture": "Architektura", + "chart_this_server": "Wykres (ten serwer)", + "containers_info": "Informacje o kontenerze", + "container_running": "uruchomiony", + "container_disabled": "Kontener zatrzymany lub wyłączony", + "container_stopped": "Zatrzymany", + "cores": "rdzenie", + "current_time": "Czas systemowy", + "disk_usage": "Użycie dysku", + "docs": "Dokumentacja", + "error_show_ip": "Nie można ustalić publicznych adresów IP", + "external_logs": "Logi zewnętrzne", + "history_all_servers": "Historia (wszystkie serwery)", + "in_memory_logs": "Logi w pamięci", + "last_modified": "Ostatnia modyfikacja", + "log_info": "

    mailcow logi w pamięci są gromadzone na listach Redis i przycinane do wartości LOG_LINES (%d) co minutę, aby ograniczyć nadmierne obciążenie systemu.

    Logi w pamięci nie są przeznaczone do trwałego przechowywania. Wszystkie aplikacje, które zapisują logi w pamięci, wysyłają je również do daemon Dockera, a więc do domyślnego sterownika logowania.

    Ten typ logów należy wykorzystywać do debugowania drobnych problemów z kontenerami.

    Logi zewnętrzne są zbierane za pośrednictwem API danej aplikacji.

    Logi statyczne to głównie dzienniki aktywności, które nie są zapisywane przez Dockerd, ale powinny być trwałe (z wyjątkiem logów API).

    ", + "login_time": "Czas", + "logs": "Logi", + "memory": "Pamięć", + "online_users": "Użytkownik online", + "restart_container": "Restart", + "service": "Usługa", + "show_ip": "Pokaż publiczne IP", + "size": "Rozmiar", + "started_at": "Zaczęło się od", + "started_on": "Zaczęło się od", + "static_logs": "Logi statyczne", + "success": "Sukces", + "system_containers": "System i kontenery", + "timezone": "Strefa czasowa", + "uptime": "Czas pracy", + "update_available": "Dostępna jest aktualizacja", + "no_update_available": "System jest w najnowszej wersji", + "update_failed": "Nie można było sprawdzić aktualizacji", + "username": "Nazwa użytkownika", + "wip": "Obecnie praca w toku" + }, + "diagnostics": { + "cname_from_a": "Wartość pochodzi z rekordu A/AAAA. Obsługiwane, o ile rekord wskazuje na prawidłowy zasób.", + "dns_records": "Rekordy DNS", + "dns_records_24hours": "Pamiętaj, że zmiany wprowadzone w DNS mogą zająć nawet do 24 godzin, zanim ich aktualny stan zostanie poprawnie odzwierciedlony na tej stronie.\nTa sekcja ma na celu ułatwienie Ci konfiguracji rekordów DNS oraz sprawdzenie, czy wszystkie rekordy zostały prawidłowo zapisane w DNS.", + "dns_records_data": "Poprawne dane", + "dns_records_docs": "Proszę skonsultuj również dokumnetację.", + "dns_records_name": "Nazwa", + "dns_records_status": "Aktualny stan", + "dns_records_type": "Typ", + "optional": "Ten rekord jest opcjonalny." + }, + "fido2": { + "confirm": "Potwierdź", + "fido2_auth": "Logowanie za pomocą FIDO2", + "fido2_success": "Urządzenie pomyślnie zarejestrowane", + "fido2_validation_failed": "Walidacja nie powiodła się", + "fn": "Przyjazna nazwa FIDO", + "known_ids": "Znane Id", + "none": "Wyłączony", + "register_status": "Status rejestracji", + "rename": "Zmień nazwę", + "set_fido2": "Zarejestruj urządzenie FIDO2", + "set_fido2_touchid": "Zarejestruj Touch ID w Apple M1", + "set_fn": "Ustaw przyjazną nazwę w FIDO", + "start_fido2_validation": "Rozpocznij walidację FIDO2" + }, + "ratelimit": { + "disabled": "Wyłączone", + "second": "wiadomości / sekundę", + "minute": "wiadomości / minutę", + "hour": "wiadomości / godzinę", + "day": "wiadomości/ dzień" } -} \ No newline at end of file +} diff --git a/mailcow/data/web/lang/lang.pt-br.json b/mailcow/data/web/lang/lang.pt-br.json index 57a4abd..b01d976 100644 --- a/mailcow/data/web/lang/lang.pt-br.json +++ b/mailcow/data/web/lang/lang.pt-br.json @@ -1,7 +1,7 @@ { "acl": { - "alias_domains": "Adicionar domínios alias", - "app_passwds": "Gerenciar senhas de aplicativos", + "alias_domains": "Adicionar alias de domínios", + "app_passwds": "Gerenciar senhas de app", "bcc_maps": "Mapas BCC", "delimiter_action": "Ação delimitadora", "domain_desc": "Alterar descrição do domínio", @@ -9,7 +9,7 @@ "eas_reset": "Redefinir dispositivos EAS", "extend_sender_acl": "Permitir estender a ACL do remetente por endereços externos", "filters": "Filtros", - "login_as": "Faça login como usuário da mailbox", + "login_as": "Fazer login como usuário da mailbox", "mailbox_relayhost": "Alterar relayhost para uma mailbox", "prohibited": "Proibido pela ACL", "protocol_access": "Alterar o acesso ao protocolo", @@ -109,7 +109,9 @@ "username": "Nome de usuário", "validate": "Validar", "validation_success": "Validado com sucesso", - "dry": "Simular sincronização" + "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." }, "admin": { "access": "Acesso", @@ -359,7 +361,57 @@ "username": "Nome de usuário", "validate_license_now": "Valide o GUID em relação ao servidor de licenças", "verify": "Verificar", - "yes": "✓" + "yes": "✓", + "iam_client_id": "ID de cliente", + "iam_client_secret": "Senha de cliente", + "iam_auth_flow": "Fluxo de autenticação", + "iam_client_scopes": "Escopo do cliente", + "iam_default_template": "Template Padrão", + "admin_quicklink": "Ocultar link rápido para página de login do administrador", + "app_hide": "Ocultar para login", + "login_page": "Página de login", + "domainadmin_quicklink": "Ocultar link rápido para página de login do administrador de domínio", + "filter": "Filtro", + "force_sso_text": "Se um provedor OIDC externo for configurado, esta opção oculta os formulários de login padrão do mailcow e mostra apenas o botão de single sign-on", + "force_sso": "Desabilitar login do mailcow e mostrar apenas single sign-on", + "iam": "Provedor de identidade", + "iam_attribute_field": "Campo de atributo", + "iam_authorize_url": "Endpoint de autorização", + "iam_auth_flow_info": "Além do fluxo de código de autorização (fluxo padrão no Keycloak), que é usado para login de single sign-on, o mailcow também suporta fluxo de autenticação com credenciais diretas. O fluxo Mailpassword tenta validar as credenciais do usuário usando a API REST do administrador do Keycloak. O mailcow recupera a senha hash do atributo mailcow_password, que é mapeado no Keycloak.", + "iam_basedn": "DN base", + "iam_default_template_description": "Se nenhum template for atribuído a um usuário, o template padrão será usado para criar a caixa de correio, mas não para atualizar a caixa de correio.", + "iam_description": "Configure um provedor externo para autenticação
    As caixas de correio dos usuários serão criadas automaticamente no primeiro login, desde que um mapeamento de atributos tenha sido definido.", + "iam_extra_permission": "Para que as configurações a seguir funcionem, o cliente mailcow no Keycloak precisa de uma conta de serviço e a permissão para visualizar usuários.", + "iam_host": "Host", + "iam_host_info": "Digite um ou mais hosts LDAP, separados por vírgulas.", + "iam_import_users": "Importar usuários", + "iam_login_provisioning": "Criar usuários automaticamente no login", + "iam_mapping": "Mapeamento de atributos", + "iam_bindpass": "Senha de vinculação", + "iam_periodic_full_sync": "Sincronização completa periódica", + "iam_port": "Porta", + "iam_realm": "Realm", + "iam_redirect_url": "URL de redirecionamento", + "iam_rest_flow": "Fluxo Mailpassword", + "iam_server_url": "URL do servidor", + "iam_sso": "Single sign-on", + "iam_sync_interval": "Intervalo de sincronização/importação (min)", + "iam_test_connection": "Testar conexão", + "iam_token_url": "Endpoint de token", + "iam_userinfo_url": "Endpoint de informações do usuário", + "iam_username_field": "Campo de nome de usuário", + "iam_binddn": "DN de vinculação", + "iam_use_ssl": "Usar SSL", + "iam_use_ssl_info": "Se habilitar SSL e a porta estiver definida como 389, ela será automaticamente substituída para usar 636.", + "iam_use_tls": "Usar StartTLS", + "iam_use_tls_info": "Se habilitar TLS, você deve usar a porta padrão para seu servidor LDAP (389). Portas SSL não podem ser usadas.", + "iam_version": "Versão", + "ignore_ssl_error": "Ignorar erros SSL", + "needs_restart": "precisa reiniciar", + "quicklink_text": "Mostrar ou ocultar links rápidos para outras páginas de login abaixo do formulário de login", + "task": "Tarefa", + "user_link": "Link do usuário", + "user_quicklink": "Ocultar link rápido para página de login do usuário" }, "danger": { "access_denied": "Acesso negado ou dados de formulário inválidos", @@ -496,7 +548,15 @@ "username_invalid": "O nome de usuário %s não pode ser usado", "validity_missing": "Por favor, atribua um período de validade", "value_missing": "Forneça todos os valores", - "yotp_verification_failed": "Falha na verificação do Yubico OTP: %s" + "yotp_verification_failed": "Falha na verificação do Yubico OTP: %s", + "authsource_in_use": "O provedor de identidade não pode ser alterado ou excluído pois está sendo usado por um ou mais usuários.", + "generic_server_error": "Ocorreu um erro inesperado no servidor. Entre em contato com seu administrador.", + "iam_test_connection": "Falha na conexão", + "max_age_invalid": "Idade máxima %s é inválida", + "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" }, "datatables": { "collapse_all": "Recolher tudo", @@ -508,7 +568,7 @@ "infoFiltered": "(filtrado do total de entradas _MAX_)", "infoPostFix": "", "thousands": ",", - "lengthMenu": "Mostrar _ MENU_ entradas", + "lengthMenu": "Mostrar _MENU_ entradas", "loadingRecords": "Carregando...", "processing": "Por favor, aguarde...", "search": "Pesquisa:", @@ -703,7 +763,25 @@ "title": "Editar objeto", "unchanged_if_empty": "Se inalterado, deixe em branco", "username": "Nome de usuário", - "validate_save": "Valide e salve" + "validate_save": "Validar e salvar", + "internal": "Interno", + "internal_info": "Aliases internos são acessíveis apenas a partir do próprio domínio ou domínios alias.", + "mailbox_rename": "Renomear caixa de correio", + "mailbox_rename_agree": "Eu criei um backup.", + "mailbox_rename_warning": "IMPORTANTE! Crie um backup antes de renomear a caixa de correio.", + "mailbox_rename_alias": "Criar alias automaticamente", + "mailbox_rename_title": "Novo nome da caixa de correio local", + "mta_sts": "MTA-STS", + "mta_sts_info": "MTA-STS é um padrão que força a entrega de email entre servidores de email para usar TLS com certificados válidos.
    É usado quando DANE não é possível devido ao DNSSEC ausente ou não suportado.
    Nota: Se o domínio de recepção suporta DANE com DNSSEC, DANE é sempre preferido – MTA-STS atua apenas como fallback.", + "mta_sts_version": "Versão", + "mta_sts_version_info": "Define a versão do padrão MTA-STS – atualmente apenas STSv1 é válido.", + "mta_sts_mode": "Modo", + "mta_sts_mode_info": "Há três modos para escolher:
    • testing – política é apenas monitorada, violações não têm impacto.
    • enforce – política é rigorosamente aplicada, conexões sem TLS válido são rejeitadas.
    • none – política é publicada mas não aplicada.
    ", + "mta_sts_max_age": "Idade máxima", + "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)." }, "fido2": { "confirm": "Confirme", @@ -766,7 +844,15 @@ "password": "Senha", "reset_password": "Recuperar a senha", "request_reset_password": "Solicitar troca de senha", - "username": "Nome de usuário" + "username": "Nome de usuário", + "login_linkstext": "Login incorreto?", + "login_usertext": "Entrar como usuário", + "login_domainadmintext": "Entrar como administrador de domínio", + "login_admintext": "Entrar como administrador", + "login_user": "Login de usuário", + "login_dadmin": "Login como administrador de domínio", + "login_admin": "Login como administrador", + "email": "Endereço de email" }, "mailbox": { "action": "Ação", @@ -941,7 +1027,9 @@ "username": "Nome de usuário", "waiting": "Esperando", "weekly": "Semanalmente", - "yes": "✓" + "yes": "✓", + "iam": "Provedor de Identidade", + "internal": "Interno" }, "oauth2": { "access_denied": "Faça login como proprietário da mailbox para conceder acesso via OAuth2.", @@ -956,8 +1044,8 @@ "action": "Ação", "atts": "Anexos", "check_hash": "Arquivo de pesquisa hash @ VT", - "confirm": "Confirme", - "confirm_delete": "Confirme a exclusão desse elemento.", + "confirm": "Confirmar", + "confirm_delete": "Confirmar exclusão desse elemento.", "danger": "Perigo", "deliver_inbox": "Entregar na caixa de entrada", "disabled_by_config": "A configuração atual do sistema desativa a funcionalidade de quarentena. Defina “retenções por mailbox” e um “tamanho máximo” para os elementos de quarentena.", @@ -1118,12 +1206,15 @@ "verified_fido2_login": "Login FIDO2 verificado", "verified_totp_login": "Login TOTP verificado", "verified_webauthn_login": "Login verificado do WebAuthn", - "verified_yotp_login": "Login OTP verificado do Yubico" + "verified_yotp_login": "Login OTP verificado do Yubico", + "custom_login_modified": "Personalização de login foi salva com sucesso", + "iam_test_connection": "Conexão bem-sucedida", + "mailbox_renamed": "Caixa de correio foi renomeada de %s para %s" }, "tfa": { "authenticators": "Autenticadores", "api_register": "%s usa a API Yubico Cloud. Obtenha uma chave de API para sua chave aqui", - "confirm": "Confirme", + "confirm": "Confirmar", "confirm_totp_token": "Confirme suas alterações inserindo o token gerado", "delete_tfa": "Desativar o TFA", "disable_tfa": "Desative o TFA até o próximo login bem-sucedido", @@ -1136,7 +1227,7 @@ "reload_retry": "- (recarregue o navegador se o erro persistir)", "scan_qr_code": "Escaneie o código a seguir com seu aplicativo autenticador ou insira o código manualmente.", "select": "Por favor, selecione", - "set_tfa": "Defina o método de autenticação de dois fatores", + "set_tfa": "Método de autenticação de dois fatores", "start_webauthn_validation": "Iniciar validação", "tfa": "Autenticação de dois fatores", "tfa_token_invalid": "Token TFA inválido", @@ -1313,7 +1404,11 @@ "weeks": "semanas", "with_app_password": "com senha do aplicativo", "year": "ano", - "years": "anos" + "years": "anos", + "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)." }, "warning": { "cannot_delete_self": "Não é possível excluir o usuário conectado", diff --git a/mailcow/data/web/lang/lang.pt-pt.json b/mailcow/data/web/lang/lang.pt-pt.json index baa90f7..e86391f 100644 --- a/mailcow/data/web/lang/lang.pt-pt.json +++ b/mailcow/data/web/lang/lang.pt-pt.json @@ -1,7 +1,7 @@ { "add": { "active": "Ativo", - "add": "Salvar", + "add": "Guardar", "alias_address": "Apelidos:", "alias_address_info": "Endereço de email completo ou @example.com, para uma conta coringa -catch all. (separado por vírgula). apenas domínios cadastrados.", "alias_domain": "Encaminhamento de Domínio", @@ -17,7 +17,7 @@ "max_mailboxes": "Máximo de contas:", "password": "Senha:", "password_repeat": "Confirmar a senha (repetir):", - "port": "Port", + "port": "Porta", "quota_mb": "Espaço (MiB):", "relay_all": "Relay para todas as contas", "relay_all_info": "Se não selecionar para retransmitir todas as contas, você deve adicionar uma (\"blind\") para cada conta que será direcionada.", @@ -27,13 +27,26 @@ "target_address": "Encaminhar para:", "target_address_info": "Endereço de email completo (separado por vírgulas).", "target_domain": "Domínio de Destino:", - "username": "Administrador" + "username": "Nome de utilizador", + "app_name": "Nome da App", + "destination": "Destino", + "generate": "gerar", + "private_comment": "Comentário privado", + "inactive": "Inativo", + "public_comment": "Comentário público", + "sieve_desc": "Breve descrição", + "sieve_type": "Tipo de filtro", + "subscribeall": "Subscrever todas as pastas", + "syncjob": "Adicionar sincronização", + "validate": "Validar", + "validation_success": "Validado com sucesso", + "app_passwd_protocols": "Protocolos permitidos para senha de aplicação" }, "admin": { "access": "Acessos", "action": "Ação", "active": "Ativo", - "add": "Salvar", + "add": "Adicionar", "add_domain_admin": "Adicionar administrador de domínio(s)", "admin": "Administrador", "admin_details": "Editar informações do administrator", @@ -58,7 +71,47 @@ "search_domain_da": "Selecione o(s) domínio(s)", "spamfilter": "Filtro de Spam", "unchanged_if_empty": "Deixar em branco para não alterar", - "username": "Administrador" + "username": "Administrador", + "add_row": "Adicionar linha", + "add_transport": "Adicionar transporte", + "change_logo": "Alterar logo", + "filter": "Filtro", + "iam_port": "Porta", + "iam_use_ssl": "Usar SSL", + "iam_version": "Versão", + "import": "Importar", + "import_private_key": "Importar chave privada", + "message": "Mensagem", + "dkim_private_key": "Chave privada", + "customize": "Personalizar", + "destination": "Destino", + "dkim_from": "De", + "activate_api": "Ativar API", + "add_admin": "Adicionar administrador", + "admins": "Administradores", + "advanced_settings": "Configurações avançadas", + "api_key": "Chave API", + "api_read_only": "Acesso leitura", + "api_read_write": "Acesso leitura e escrita", + "login_page": "Página de login", + "dkim_key_missing": "Chave em falta", + "dkim_key_valid": "Chave válida", + "dkim_to": "Para", + "domain_admin": "Administrador de dominio", + "domain_s": "Dominio(s)", + "duplicate": "Duplicar", + "duplicate_dkim": "Duplicar registo DKIM", + "empty": "Sem resultados", + "excludes": "Excluir estes recipientes", + "f2b_filter": "Filtros de regex", + "from": "De", + "generate": "gerar", + "html": "HTML", + "iam_import_users": "Importar Utilizadores", + "ui_header_announcement_content": "Texto (HTML permitido)", + "ui_footer": "Rodapé (HTML permitido)", + "activate_send": "Ativar botão de envio", + "help_text": "Anular texto de ajuda abaixo da máscara de login (HTML permitido)" }, "danger": { "access_denied": "Acesso negado ou dados inválidos", @@ -97,7 +150,10 @@ "target_domain_invalid": "O endereço de Domínio Destino é inválido", "targetd_not_found": "Domínio de Destino não encontrado", "username_invalid": "Nome de utilizador inválido", - "validity_missing": "Você deve definir um período de validade" + "validity_missing": "Você deve definir um período de validade", + "comment_too_long": "Comentário demasiado longo, máximo 160 chars permitidos", + "ip_list_empty": "Lista de IPs permitidos não pode estar vazia", + "last_key": "A última tecla não pode ser apagada, por favor desactivar a TFA." }, "edit": { "active": "Ativo", @@ -129,7 +185,9 @@ "target_domain": "Domínio de Destino:", "title": "Editar dos Objetos", "unchanged_if_empty": "Deixar em branco para não modificar", - "username": "Administrador" + "username": "Administrador", + "app_passwd_protocols": "Protocolos permitidos para palavra-passe de aplicação", + "allowed_protocols": "Protocolos permitidos para acesso direto do utilizador (não afeta os protocolos de palavra-passe da aplicação)" }, "footer": { "loading": "Aguarde..." @@ -186,11 +244,15 @@ "target_domain": "Domínio Destino", "tls_enforce_in": "Forçar TLS na entrada", "tls_enforce_out": "Forçar TLS na saída", - "username": "Utilizador" + "username": "Utilizador", + "allowed_protocols": "Protocolos permitidos", + "activate": "Ativar", + "deactivate": "Desativar" }, "quarantine": { "action": "Ação", - "remove": "Remover" + "remove": "Remover", + "check_hash": "Pesquisar hash de ficheiro @ VT" }, "queue": { "queue_manager": "Queue Manager" @@ -269,6 +331,31 @@ "user_settings": "Configurações do utilizador", "username": "Administrador", "week": "Semana", - "weeks": "Semanas" + "weeks": "Semanas", + "aliases_also_send_as": "Também autorizado a enviar como utilizador", + "app_hint": "As senhas de aplicação são senhas alternativas para o seu login IMAP, SMTP, CalDAV, CardDAV e EAS. O nome de utilizador permanece inalterado. O webmail SOGo não está disponível através de passwords de aplicação.", + "allowed_protocols": "Protocolos permitidos" + }, + "acl": { + "tls_policy": "Política de TLS", + "quarantine_attachments": "Anexos de quarentena", + "filters": "Filtros", + "smtp_ip_access": "Mudar anfitriões permitidos para SMTP" + }, + "warning": { + "no_active_admin": "Não é possível desactivar o último administrador activo" + }, + "tfa": { + "u2f_deprecated": "Parece que a sua chave foi registada usando o método U2F depreciado. Desactivaremos dois factores-autenticação para si e apagaremos a sua Chave.", + "none": "Desativar" + }, + "datatables": { + "search": "Procurar:", + "info": "A mostrar _START_ a _END_ de _TOTAL_ entradas", + "infoEmpty": "A mostrar 0 a 0 de 0 entradas", + "aria": { + "sortAscending": ": activar para ordenar a coluna ascendente", + "sortDescending": ": activar para ordenar a coluna descendente" + } } -} \ No newline at end of file +} diff --git a/mailcow/data/web/lang/lang.ru-ru.json b/mailcow/data/web/lang/lang.ru-ru.json index e9d8569..e7d8256 100644 --- a/mailcow/data/web/lang/lang.ru-ru.json +++ b/mailcow/data/web/lang/lang.ru-ru.json @@ -109,7 +109,9 @@ "timeout2": "Тайм-аут для подключения к локальному хосту", "username": "Имя пользователя", "validate": "Проверить", - "validation_success": "Проверка прошла успешно" + "validation_success": "Проверка прошла успешно", + "internal": "Внутренний", + "internal_info": "Внутренние псевдонимы доступны только из самого домена или доменов-псевдонимов." }, "admin": { "access": "Настройки доступа", @@ -184,7 +186,7 @@ "excludes": "Исключает этих получателей", "f2b_ban_time": "Время бана (в секундах)", "f2b_ban_time_increment": "Время бана увеличивается с каждым баном", - "f2b_blacklist": "Черный список подсетей/хостов", + "f2b_blacklist": "Черный список сетей/хостов", "f2b_filter": "Правила фильтрации с помощью регулярных выражений", "f2b_list_info": "Хосты или подсети, занесенные в черный список, всегда будут перевешивать объекты из белого списка. Обновление списка займет несколько секунд.", "f2b_manage_external": "Внешнее управление Fail2Ban", @@ -196,7 +198,7 @@ "f2b_parameters": "Настройки Fail2ban", "f2b_regex_info": "Журналы которые принимаются во внимание: SOGo, Postfix, Dovecot, PHP-FPM.", "f2b_retry_window": "Промежуток времени для следующего бана (в секундах)", - "f2b_whitelist": "Белый список подсетей/хостов", + "f2b_whitelist": "Белый список сетей/хостов", "filter_table": "Поиск", "forwarding_hosts": "Переадресация хостов", "forwarding_hosts_add_hint": "Можно указывать: IPv4/IPv6 подсети в нотации CIDR, имена хостов (которые будут разрешаться в IP-адреса) или доменные имена (которые будут решаться с IP-адресами путем запроса SPF записей или, в случае их отсутствия - запросом MX записей).", @@ -319,7 +321,7 @@ "rspamd_global_filters": "Глобальные правила фильтрации", "rspamd_global_filters_agree": "Я понимаю, что я делаю, и буду осторожен!", "rspamd_global_filters_info": "Глобальные правила фильтрации содержат различные виды глобальных черных и белых списков.", - "rspamd_global_filters_regex": "Названия фильтров отражают их предназначение. Все правила должены состоять из регулярных выражений в формате \"/pattern/options\" (например: /.+@domain\\.tld/i).
    \r\nНесмотря на то, что перед сохранением правил выполняется проверка регулярных выражений, функциональность Rspamds может быть нарушена, если будет использован
    \r\n некорректный синтаксис. Будьте внимательны при написании правил.
    Электронные письма от адресов электронной почты, проходящие по регулярным выражениям черных списков, будут отклонены без сохранения в карантин.
    \r\n Rspamd попытается прочитать содержимое правил при их изменении. Но, если что, вы можете перезапустить Rspamd, чтобы принять последние изменения принудительно.", + "rspamd_global_filters_regex": "Названия фильтров отражают их предназначение. Все правила должны состоять из регулярных выражений в формате \"/pattern/options\" (например: /.+@domain\\.tld/i).
    \nНесмотря на то, что перед сохранением правил выполняется проверка регулярных выражений, функциональность Rspamds может быть нарушена, если будет использован
    \n некорректный синтаксис. Будьте внимательны при написании правил.
    Электронные письма от адресов электронной почты, проходящие по регулярным выражениям черных списков, будут отклонены без сохранения в карантин.
    \n Rspamd попытается прочитать содержимое правил при их изменении. Но, если что, вы можете перезапустить Rspamd, чтобы принять последние изменения принудительно.", "rspamd_settings_map": "Правила Rspamd", "sal_level": "Уровень Муу", "save": "Сохранить изменения", @@ -359,7 +361,57 @@ "username": "Имя пользователя", "validate_license_now": "Получить лицензию на основе GUID с сервера лицензий", "verify": "Проверить", - "yes": "✓" + "yes": "✓", + "force_sso_text": "Если настроен внешний провайдер OIDC, эта опция скрывает стандартные формы входа mailcow и показывает только кнопку Singe Sign-On", + "domainadmin_quicklink": "Скрыть ссылку на вход для администраторов домена", + "iam_periodic_full_sync": "Периодическая полная синхронизация", + "iam_sync_interval": "Интервал синхронизации/импорта (мин)", + "iam_use_tls_info": "При включённом TLS необходимо использовать стандартный порт LDAP-сервера (389). Порты SSL не подходят.", + "iam_use_ssl_info": "При включённом SSL, если указан порт 389, то вместо него автоматически будет использоваться порт 636.", + "quicklink_text": "Показать или скрыть ссылки для быстрого перехода к другим страницам входа под формой авторизации", + "iam_login_provisioning": "Автоматическое создание пользователей при входе в систему", + "iam_mapping": "Сопоставление атрибутов", + "iam_bindpass": "Bind пароль", + "iam_port": "Порт", + "iam_realm": "Realm", + "iam_redirect_url": "URL переадресации", + "iam_rest_flow": "Поток Mailpassword", + "iam_server_url": "URL сервера", + "iam_sso": "Технология единого входа (SSO)", + "iam_token_url": "Token endpoint", + "iam_userinfo_url": "User info endpoint", + "iam_username_field": "Поле имени пользователя", + "iam_binddn": "Bind DN", + "iam_use_ssl": "Использовать SSL", + "iam_use_tls": "Использовать StartTLS", + "iam_version": "Версия", + "ignore_ssl_error": "Игнорировать ошибки SSL", + "task": "Задача", + "user_link": "Пользовательская ссылка", + "user_quicklink": "Скрыть ссылку на вход для пользователей", + "app_hide": "Скрыть для входа", + "iam_test_connection": "Проверка соединения", + "login_page": "Страница входа", + "filter": "Фильтр", + "force_sso": "Отключить mailcow авторизацию и показывать только Singe Sign-On", + "iam": "Провайдер идентификации", + "iam_attribute_field": "Поле атрибута", + "iam_authorize_url": "Конечная точка авторизации", + "iam_auth_flow": "Процесс аутентификации", + "iam_auth_flow_info": "В дополнение к потоку кода авторизации (стандартный поток в Keycloak), который используется для Single-Sign On входа, mailcow также поддерживает поток аутентификации с непосредственными учетными данными. Поток Mailpassword пытается проверить учетные данные пользователя с помощью REST API администратора Keycloak. mailcow извлекает хешированный пароль из атрибута mailcow_password, который отображается в Keycloak.", + "iam_basedn": "Base DN", + "iam_client_id": "ID клиента", + "iam_client_secret": "Секрет клиента", + "iam_client_scopes": "Области действия клиента", + "iam_default_template": "Шаблон по умолчанию", + "iam_default_template_description": "Если пользователю не назначен шаблон, шаблон по умолчанию будет использоваться при создании почтового ящика, но не при обновлении почтового ящика.", + "iam_description": "Настройка внешнего провайдера для аутентификации
    Почтовые ящики пользователей будут автоматически создаваться при первом входе в систему, если было задано сопоставление атрибутов.", + "iam_extra_permission": "Для работы следующих настроек клиенту mailcow в Keycloak необходима служебная учетная запись и разрешение на просмотр пользователей.", + "iam_host": "Хост", + "iam_host_info": "Укажите один или несколько LDAP-хостов через запятую.", + "iam_import_users": "Импорт пользователей", + "admin_quicklink": "Скрыть ссылку на вход для администраторов", + "needs_restart": "необходим перезапуск" }, "danger": { "access_denied": "Доступ запрещён, или указаны неверные данные", @@ -496,7 +548,15 @@ "webauthn_publickey_failed": "Для выбранного аутентификатора не был сохранен открытый ключ", "webauthn_username_failed": "Выбранный аутентификатор принадлежит другой учетной записи", "webauthn_verification_failed": "Ошибка валидации WebAuthn: %s", - "yotp_verification_failed": "Ошибка валидации Yubico OTP: %s" + "yotp_verification_failed": "Ошибка валидации Yubico OTP: %s", + "generic_server_error": "На сервере произошла непредвиденная ошибка. Пожалуйста, свяжитесь с вашим администратором.", + "authsource_in_use": "Поставщик идентификационных данных не может быть изменен или удален, так как в данный момент он используется одним или несколькими пользователями.", + "iam_test_connection": "Ошибка соединения", + "required_data_missing": "Отсутствуют необходимые данные %s", + "max_age_invalid": "Максимальный возраст %s недействителен", + "mode_invalid": "Режим %s недействителен", + "mx_invalid": "Запись MX %s недействительна", + "version_invalid": "Версия %s недействительна" }, "datatables": { "collapse_all": "Свернуть все", @@ -581,7 +641,7 @@ "alias": "Изменить псевдоним", "allow_from_smtp": "Разрешить использование SMTP только для этих IP", "allow_from_smtp_info": "Укажите IPv4/IPv6 адреса и/или подсети.
    Оставьте поле пустым, чтобы разрешить отправку с любых адресов.", - "allowed_protocols": "Разрешённые протоколы", + "allowed_protocols": "Разрешённые протоколы для прямого доступа пользователей (не влияет на протоколы паролей приложений)", "app_name": "Название приложения", "app_passwd": "Пароль приложения", "app_passwd_protocols": "Разрешенные протоколы для пароля приложения", @@ -697,7 +757,7 @@ "sogo_visible_info": "Влияет только на объекты, которые могут отображаться в SOGo (персональные или общие псевдонимы, указывающие как минимум на один локальный почтовый аккаунт). Учтите, что если функция отключена, у пользователей не будет возможности выбрать адрес псевдонима в качестве отправителя в SOGo.", "spam_alias": "Создать или изменить временные (спам) псевдонимы", "spam_filter": "Спам фильтр", - "spam_policy": "Добавление или удаление элементов в белом/черном списке", + "spam_policy": "Добавить или удалить элементы белого/черного списка", "spam_score": "Задать индивидуальное определение спама", "subfolder2": "Синхронизировать в подпапку
    (пусто = в корень)", "syncjob": "Изменить задание синхронизации", @@ -708,7 +768,20 @@ "title": "Изменение объекта", "unchanged_if_empty": "Если без изменений - оставьте пустым", "username": "Имя пользователя", - "validate_save": "Подтвердить и сохранить" + "validate_save": "Подтвердить и сохранить", + "internal": "Внутренний", + "internal_info": "Внутренние псевдонимы доступны только из самого домена или доменов-псевдонимов.", + "mta_sts": "MTA-STS", + "mta_sts_info": "MTA-STS — это стандарт, который обязывает почтовые серверы использовать TLS с подлинными сертификатами для доставки электронной почты.
    Он используется, когда DANE невозможен из-за неиспользуемого или неподдерживаемого DNSSEC.
    Примечание: если принимающий домен поддерживает DANE с DNSSEC, всегда предпочитается DANE — MTA-STS действует только как резервный вариант.", + "mta_sts_version": "Версия", + "mta_sts_version_info": "Определяет версию стандарта MTA-STS – на данный момент существует только STSv1.", + "mta_sts_mode": "Режим", + "mta_sts_mode_info": "Есть три режима на выбор:
    • testing – политика только наблюдается, нарушения не имеют последствий.
    • enforce – политика соблюдается строго, соединения без подлинного TLS отклоняются.
    • none – политика опубликована, но не применяется.
    ", + "mta_sts_max_age": "Максимальный возраст", + "mta_sts_max_age_info": "Время в секундах, в течение которого принимающие почтовые серверы могут кэшировать эту политику перед повторной загрузкой.", + "mta_sts_mx": "Сервер MX", + "mta_sts_mx_info": "Разрешает отправку только на явно указанные имена хостов почтовых серверов; отправляющий MTA проверяет, соответствует ли DNS-имя MX-хоста списку политик, и разрешает доставку только с подлинным TLS-сертификатом (защита от MITM).", + "mta_sts_mx_notice": "Можно указать несколько MX-серверов (через запятую)." }, "fido2": { "confirm": "Подтвердить", @@ -771,7 +844,15 @@ "password": "Пароль", "request_reset_password": "Запросить восстановление пароля", "reset_password": "Восстановление пароля", - "username": "Имя пользователя" + "username": "Имя пользователя", + "login_linkstext": "Неправильный логин?", + "login_usertext": "Войти как пользователь", + "login_domainadmintext": "Войти как администратор домена", + "login_admintext": "Войти как администратор", + "login_user": "Вход для пользователей", + "login_dadmin": "Вход для администраторов домена", + "login_admin": "Вход для администраторов", + "email": "Email-адрес" }, "mailbox": { "action": "Действия", @@ -946,7 +1027,9 @@ "username": "Имя пользователя", "waiting": "В ожидании", "weekly": "Раз в неделю", - "yes": "✓" + "yes": "✓", + "iam": "Поставщик идентификационных данных", + "internal": "Внутренний" }, "oauth2": { "access_denied": "Пожалуйста, войдите в систему как владелец почтового аккаунта, чтобы получить доступ через OAuth2.", @@ -978,7 +1061,7 @@ "notified": "Увед.", "qhandler_success": "Запрос успешно отправлен в систему. Теперь вы можете закрыть окно.", "qid": "Rspamd QID", - "qinfo": "Карантин сохраняет входящие сообщения, классифицированные как нежелательные, в базу данных.\r\n
    Отправители писем, которые помечены как отвергнутые, будут уверены что их письма не были доставлены вам.\r\n
    \"Освободить из карантина\" изучит сообщение как полезную почту; по теореме Байеса и доставит его вам в Inbox.\r\n
    \"Запомнить как спам и удалить\" изучит сообщение как спам по теореме Байеса, а также вычислит нечёткие хэши, чтобы лучше блокировать подобные сообщения в дальнейшем.\r\n
    Учтите, что в зависимости от технических характеристик вашей системы, изучение большого количества сообщений может занять много времени.", + "qinfo": "Карантин сохраняет входящие сообщения, классифицированные как нежелательные, в базу данных.\n
    Отправители писем, которые помечены как отвергнутые, будут уверены что их письма не были доставлены вам.\n
    \"Освободить из карантина\" изучит сообщение как полезную почту; по теореме Байеса и доставит его вам в Inbox.\n
    \"Запомнить как спам и удалить\" изучит сообщение как спам по теореме Байеса, а также вычислит нечёткие хэши, чтобы лучше блокировать подобные сообщения в дальнейшем.\n
    Учтите, что в зависимости от технических характеристик вашей системы, изучение большого количества сообщений может занять много времени.", "qitem": "Обьект карантина", "quarantine": "Карантин", "quick_actions": "Действия", @@ -1124,7 +1207,9 @@ "verified_fido2_login": "Авторизация FIDO2 пройдена", "verified_totp_login": "Авторизация TOTP пройдена", "verified_webauthn_login": "Авторизация WebAuthn пройдена", - "verified_yotp_login": "Авторизация Yubico OTP пройдена" + "verified_yotp_login": "Авторизация Yubico OTP пройдена", + "iam_test_connection": "Успешное соединение", + "custom_login_modified": "Настройки входа успешно сохранены" }, "tfa": { "api_register": "%s использует Yubico Cloud API. Пожалуйста, получите ключ API для вашего ключа здесь", @@ -1191,7 +1276,7 @@ "delete_ays": "Пожалуйста, подтвердите удаление", "direct_aliases": "Личные псевдонимы", "direct_aliases_desc": "На личные псевдонимы распространяются фильтры нежелательной почты и параметры политики TLS.", - "direct_protocol_access": "Этот пользователь почтового ящика имеет прямой, внешний доступ к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.
    Кнопка \"Вход в веб-почту\" обеспечивает единый вход в SOGo и всегда доступна.", + "direct_protocol_access": "Этот пользователь почтового ящика имеет прямой, внешний доступ к следующим протоколам и приложениям. Эта настройка контролируется вашим администратором. Для предоставления доступа к отдельным протоколам и приложениям могут быть созданы пароли приложений.
    Кнопка \"Веб-почта\" обеспечивает единый вход в SOGo и всегда доступна.", "eas_reset": "Сбросить кеш ActiveSync устройств", "eas_reset_help": "Во многих случаях сброс кеша устройств помогает восстановить повреждённый профиль ActiveSync.
    Внимание: все письма, календари и контакты будут загружены заново на все ваши устройства!", "eas_reset_now": "Сбросить кеш сейчас", @@ -1267,7 +1352,7 @@ "sogo_profile_reset": "Сбросить профиль SOGo", "sogo_profile_reset_help": "Внимание: это удалит настройки профиля SOGo вместе с всеми контактами, календарями и фильтрами безвозвратно.", "sogo_profile_reset_now": "Сбросить профиль сейчас", - "spam_aliases": "Временные псевдонимы электронной почты", + "spam_aliases": "Псевдонимы для спама", "spam_score_reset": "Сброс на настройки по умолчанию", "spamfilter": "Спам фильтр", "spamfilter_behavior": "Фильтрация спама", @@ -1319,7 +1404,14 @@ "weeks": "недели", "with_app_password": "с паролем приложения", "year": "год", - "years": "лет" + "years": "лет", + "authentication": "Аутентификация", + "tfa_info": "Двухфакторная аутентификация помогает защитить вашу учетную запись. Если вы включите эту функцию, вам понадобятся пароли приложений для входа в приложения или службы, которые не поддерживают двухфакторную аутентификацию (например, почтовые клиенты).", + "protocols": "Протоколы", + "overview": "Обзор", + "expire_never": "Никогда не истекает", + "forever": "Навсегда", + "spam_aliases_info": "Псевдоним для спама — это временный адрес электронной почты, который можно использовать для защиты реальных адресов.
    При желании можно установить срок действия, по истечении которого псевдоним будет автоматически деактивирован, что позволяет эффективно избавляться от адресов, которые были использованы не по назначению или стали доступны посторонним лицам." }, "warning": { "cannot_delete_self": "Вы не можете удалить сами себя", diff --git a/mailcow/data/web/lang/lang.si-si.json b/mailcow/data/web/lang/lang.si-si.json index b5a093a..918fb8b 100644 --- a/mailcow/data/web/lang/lang.si-si.json +++ b/mailcow/data/web/lang/lang.si-si.json @@ -20,7 +20,7 @@ "sogo_access": "Dovoli upravljanje SOGo dostopa", "sogo_profile_reset": "Ponastavi SOGo profil", "spam_alias": "Začasni vzdevki", - "spam_policy": "Črna lista/Bela lista", + "spam_policy": "Seznam zavrnjenih/dovoljenih", "spam_score": "Ocena neželene pošte", "tls_policy": "Politika TLS", "unlimited_quota": "Neomejena kvota za poštne predale", @@ -42,7 +42,7 @@ "app_name": "Ime aplikacije", "app_password": "Dodaj geslo aplikacije", "app_passwd_protocols": "Dovoljeni protokoli za geslo aplikacije", - "automap": "Poskusi samodejno preslikati mape (\"Sent items\", \"Sent\" => \"Poslano\" ipd.)", + "automap": "Poskusi samodejno preslikati mape (\"Poslani elementi\", \"Poslano\" => \"Poslano\" ipd.)", "backup_mx_options": "Možnosti posredovanja (relay)", "comment_info": "Zasebni komentarji niso vidni uporabnikom, javni komentarji pa so prikazani kot opis, ko se z miško postavimo nad uporabnika v pregledu", "custom_params": "Parametri po meri", @@ -109,7 +109,9 @@ "relay_transport_info": "
    Info
    Definirate lahko preslikave transportov za cilj po meri za to domeno. Če ni nastavljena, se ustvari MX poizvedba.", "syncjob_hint": "Pozor! Gesla se morajo shraniti v golo besedilo!", "timeout2": "Časovna omejitev za povezavo do lokalnega gostitelja", - "dry": "Simuliraj sinhronizacijo" + "dry": "Simuliraj sinhronizacijo", + "internal": "Notranje", + "internal_info": "Notranji vzdevki so dostopni samo iz lastne domene ali vzdevkov domen." }, "admin": { "access": "Dostop", @@ -150,7 +152,7 @@ "customize": "Prilagodi", "destination": "Cilj", "dkim_add_key": "Dodaj ARC/DKIM ključ", - "dkim_domains_selector": "Izbira", + "dkim_domains_selector": "Izbirnik", "dkim_domains_wo_keys": "Izberi domene z manjkajočimi ključi", "dkim_from": "Od", "dkim_from_title": "Izvorna domena od katere prekopiram podatke", @@ -172,56 +174,56 @@ "excludes": "Izključuje te prejemnike", "f2b_ban_time": "Čas blokade (s)", "f2b_ban_time_increment": "Čas blokade se poveča z vsako blokado", - "f2b_blacklist": "Mreže/gostitelji na blacklisti", + "f2b_blacklist": "Omrežja/gostitelji na seznamu zavrnjenih", "f2b_filter": "Regex filtri", "f2b_max_attempts": "Največ poskusov", "f2b_max_ban_time": "Maksimalno trajanje blokade (s)", - "f2b_netban_ipv4": "velikost subneta IPv4 za blokiranje (8-32)", - "f2b_netban_ipv6": "Velikost subneta IPv6 za blokiranje (8-128)", + "f2b_netban_ipv4": "velikost podomrežja IPv4 za blokiranje (8-32)", + "f2b_netban_ipv6": "Velikost podomrežja IPv6 za blokiranje (8-128)", "f2b_parameters": "Fail2ban parametri", "f2b_regex_info": "Upoštevajo se dnevniki SOGo, Postfix, Dovecot, PHP-FPM.", "f2b_retry_window": "Upoštevan čas (s) za največ poskusov", - "f2b_whitelist": "Mreže/gostitelji na whitelisti", + "f2b_whitelist": "Omrežja/gostitelji na seznamu dovoljenih", "filter_table": "Filtriraj tabelo", "from": "Od", "generate": "ustvari", "guid": "GUID - enolični ID instance", "guid_and_license": "GUID & licenca", - "hash_remove_info": "Odstranitev hasha za omejitev (če obstaja) bo povsem ponastavilo njen števec.
    \n Vsak hash je prikazan z individualno barvo.", - "help_text": "Zamenjaj tekst za pomoč pod masko za prijavo (HTML je dovoljen)", + "hash_remove_info": "Odstranitev zgoščene vrednosti za omejitev (če obstaja) bo povsem ponastavilo njen števec.
    \n Vsaka zgoščena vrednost je prikazana z individualno barvo.", + "help_text": "Preglasi besedilo za pomoč pod masko za prijavo (HTML je dovoljen)", "host": "Gostitelj", "html": "HTML", "import": "Uvozi", "import_private_key": "Uvozi zasebni ključ", - "in_use_by": "V uporabi", + "in_use_by": "V uporabi od", "inactive": "Neaktivno", "include_exclude": "Vključi/Izključi", "include_exclude_info": "Privzeto - če ni izbire - so vključeni vsi poštni predali", "includes": "Vključi te prejemnike", - "ip_check": "Kontrola IP", - "ip_check_disabled": "Kontrola IP je onemogočena. Lahko jo omogočite pod
    Sistem > Konfiguracija > Možnosti > Prilagodi", - "ip_check_opt_in": "Opt-in za uporabo zunanje storitve ipv4.mailcow.email in ipv6.mailcow.email za razreševanje zunanjih IP.", - "is_mx_based": "Glede na MX", - "last_applied": "Nazadnje aplicirano", + "ip_check": "Preverjanje IP-ja", + "ip_check_disabled": "Preverjanje IP-ja je onemogočeno. Lahko ga omogočite pod
    Sistem > Konfiguracija > Možnosti > Prilagodi", + "ip_check_opt_in": "Prijavite se za uporabo storitev tretjih oseb ipv4.mailcow.email in ipv6.mailcow.email za razreševanje zunanjih IP naslovov.", + "is_mx_based": "Glede na MX zapis", + "last_applied": "Nazadnje uporabljeno", "link": "Povezava", "loading": "Prosim počakajte...", "login_time": "Čas prijave", - "logo_info": "Vaša slika bo pomanjšana na velikost 40px za zgornjo navigacijo in največjo velikost 250px za začetno stran. Zelo priporočena je uporaba grafike brez izgube kakovosti ob spremembi velikosti.", + "logo_info": "Vaša slika bo pomanjšana na višino 40 slikovnih pik za zgornjo navigacijsko vrstico in na največjo širino 250 slikovnih pik za začetno stran. Zelo priporočljiva je skalabilna grafika.", "message": "Sporočilo", "message_size": "Velikost sporočila", "nexthop": "Naslednji skok", "no": "✕", "no_active_bans": "Ni aktivnih blokad", - "no_new_rows": "Ni dodatnih vrstic", + "no_new_rows": "Nadaljnjih vrstic ni na voljo", "no_record": "Ni zapisa", "oauth2_apps": "OAuth2 aplikacije", "oauth2_add_client": "Dodaj OAuth2 klienta", - "oauth2_client_id": "ID klienta", - "oauth2_client_secret": "Skrivnost (secret)", + "oauth2_client_id": "ID odjemalca", + "oauth2_client_secret": "Skrivnost odjemalca", "oauth2_redirect_uri": "URI za preusmeritev", - "oauth2_renew_secret": "Generiraj nov client secret", - "oauth2_revoke_tokens": "Zavrni vse tokene klientov", - "optional": "opcijsko", + "oauth2_renew_secret": "Generiraj novo skrivnost odjemalca", + "oauth2_revoke_tokens": "Prekliči vse žetone odjemalca", + "optional": "neobvezno", "options": "Možnosti", "password": "Geslo", "password_length": "Dolžina gesla", @@ -236,21 +238,21 @@ "private_key": "Zasebni ključ", "quarantine": "Karantena", "quarantine_bcc": "Pošlji kopijo vseh obvestil (BCC) temu prejemniku:
    Pustite prazno za izklop te funkcije. Nepodpisana, nepreverjena pošta. Uporabljalo naj bi se samo za interno dostavo.", - "quarantine_exclude_domains": "Izključi domene in alias-domene", - "quarantine_max_age": "Maksimalna starost v dnevnih
    Vrednost mora biti večja ali enaka 1 dnevu", - "quarantine_max_score": "Opusti obvestilo, če je ocena spama večja od te vrednosti:
    Privzeto 9999.0", - "quarantine_max_size": "Največja velikost v MiB (Večji elementi so zavrženi):
    0 ne pomeni neomejeno.", - "quarantine_notification_html": "Predloga sporočila za obvestilo:
    Pustite prazno za obnovitev privzete predloge.", - "quarantine_notification_sender": "Pošiljatelj obvestila", - "quarantine_notification_subject": "Naslov obvestila", - "quarantine_release_format": "Oblika sproščenih elementov", + "quarantine_exclude_domains": "Izključi domene in vzdevke domen", + "quarantine_max_age": "Najvišja starost v dneh
    Vrednost mora biti enaka ali večja od 1 dneva.", + "quarantine_max_score": "Zavrzi obvestilo, če je ocena neželene pošte višja od te vrednosti:
    Privzeto 9999,0", + "quarantine_max_size": "Največja velikost v MiB (večji elementi so zavrženi):
    0 ne pomeni neomejeno.", + "quarantine_notification_html": "Predloga za obvestilo po e-pošti:
    Pustite prazno, če želite obnoviti privzeto predlogo.", + "quarantine_notification_sender": "Pošiljatelj obvestil po e-pošti", + "quarantine_notification_subject": "Zadeva e-poštnega obvestila", + "quarantine_release_format": "Oblika izdanih elementov", "quarantine_release_format_att": "Kot priponka", - "quarantine_release_format_raw": "Nespremenjen original", - "quarantine_retention_size": "Število zadržanj na poštni predal:
    0 pomeni neaktivno,", + "quarantine_release_format_raw": "Nespremenjen izvirnik", + "quarantine_retention_size": "Hrambe na poštni predal:
    0 pomeni neaktivno.", "quota_notification_sender": "Pošiljatelj obvestila", "quota_notification_subject": "Predmet obvestila", - "quota_notifications": "Obvestila o omejitvi", - "quota_notifications_info": "Obvestila o omejitvi so poslana uporabnikom enkrat, ko presežejo 80% in enkrat ko presežejo 95% zasedenosti.", + "quota_notifications": "Obvestila o kvotah", + "quota_notifications_info": "Obvestila o kvoti se uporabnikom pošljejo enkrat, ko presežejo 80 % in enkrat, ko presežejo 95 % porabe.", "queue_unban": "odblokiraj", "r_active": "Aktivne omejitve", "r_inactive": "Neaktivne omejitve", @@ -266,8 +268,8 @@ "remove": "Odstrani", "remove_row": "Odstrani vrstico", "reset_default": "Ponastavi na privzeto", - "reset_limit": "Odstrani hash", - "routing": "Routing", + "reset_limit": "Odstrani zgoščeno vrednost", + "routing": "Usmerjanje", "rsetting_add_rule": "Dodaj pravilo", "rsetting_content": "Vsebina pravila", "rsetting_desc": "Kratek opis", @@ -275,48 +277,48 @@ "rsetting_none": "Ni pravil na voljo", "rsettings_insert_preset": "Vstavi prednastavljen primer \"%s\"", "rsettings_preset_1": "Onemogoči vse razen DKIM in omejitve za prijavljene uporabnike", - "rsettings_preset_2": "Postmasterji želijo spam", - "rsettings_preset_3": "Dovoli samo specifične pošiljatelje za poštni predal (npr. uporaba samo kot interni poštni predal)", + "rsettings_preset_2": "Poštni upravitelji želijo neželeno pošto", + "rsettings_preset_3": "Dovoli samo določene pošiljatelje za poštni predal (tj. uporabo samo kot notranji poštni predal)", "rsettings_preset_4": "Onemogoči Rspamd za domeno", - "rspamd_com_settings": "Ime nastavitve bo samodejno generirano. Prosim oglejte si primere nastavitev spodaj. Za več informacij si oglejte dokumentacijo Rspamd", + "rspamd_com_settings": "Ime nastavitve bo samodejno ustvarjeno, oglejte si spodnje primere prednastavitev. Za več podrobnosti glejte dokumentacijo Rspamd", "rspamd_global_filters": "Globalne preslikave filtrov", "rspamd_global_filters_agree": "Previden bom!", - "rspamd_global_filters_info": "Globalne preslikave filtrov vsebujejo različne vrste globalnih blacklist in whitelist.", + "rspamd_global_filters_info": "Globalni filtri vsebujejo različne vrste globalnih seznamov zavrnjenih in dovoljenih vsebin.", "add_admin": "Dodaj skrbnika", "add_relayhost_hint": "Prosimo zavedajte se, da se podatki za avtentikacijo, če obstajajo, shranijo v golo besedilo.", "admin": "Skrbnik", "api_allow_from": "Dovoli API dostop s teh IP naslovov / CIDR mrežnih zapisov", "apps_name": "Ime aplikacije v mailcow", - "ban_list_info": "Oglejte si seznam blokiranih IP naslovov spodaj: network (remaining ban time) - [actions].
    . IPji v vrsti za odstranitev blokade bodo odstranjeni iz aktivnega seznama blokad v nekaj sekundah.
    Rdeče oznake prikazujejo trajne blokade z blacklisto.", + "ban_list_info": "Spodaj si oglejte seznam prepovedanih IP-jev: omrežje (preostali čas prepovedi) - [dejanja].
    IP-ji, ki so v čakalni vrsti za odpravo prepovedi, bodo v nekaj sekundah odstranjeni s seznama aktivnih prepovedi.
    Rdeče oznake označujejo aktivne trajne prepovedi s seznama zavrnjenih.", "dkim_key_length": "Dolžina DKIM ključa (v bitih)", "dkim_to_title": "Ciljne domene bodo prepisane", - "f2b_list_info": "Gostitelj ali omrežje na blacklisti bo vedno prevladal zapis na whitelisti. Apliciranje sprememb seznama traja nekaj sekund.", + "f2b_list_info": "Gostitelj ali omrežje na seznamu zavrnjenih bo vedno imelo prednost pred entiteto na seznamu dovoljenih. Posodobitve seznama bodo trajale nekaj sekund, da se uporabijo.", "forwarding_hosts": "Gostitelji za posredovanje", "forwarding_hosts_add_hint": "Lahko vpišete IPv4/IPv6 naslove, mreže v CIDR obliki, imena gostiteljev (kateri se prevedejo v IP naslove) ali imena domen (katera se prevedejo v IP naslove glede na poizvedbo po SPF zapisih, v primeru manjkajočih zapisov pa MX zapisih).", - "forwarding_hosts_hint": "Dohodna sporočila so brezpogojno sprejeta od katerih koli gostiteljev v tem seznamu. Ti gostitelji se ne bodo preverjali po DNSBL seznamih in ne bodo dodani v greyliste. Prejeti spam s teh gostiteljev ni nikoli zavrnjen, opcijsko pa se lahko premakne v mapo neželene pošte. Najpogostejša uporaba za to je navedba poštnih strežnikov, iz katerih ste nastavili pravilo za posredovanje pošte na vaš mailcow strežnik.", - "license_info": "Licenca ni zahtevana, a pomaga pri nadaljnjem razvoju.
    Registrirajte svoj GUID tukaj ali Kupite podporo za svojo namestitev Mailcow.", - "lookup_mx": "Cilj je regular expression za ujemanje MX zapisov (.*\\.google\\.com za usmeritev vse pošte na MX, ki se konča z google.com, preko tega skoka)", + "forwarding_hosts_hint": "Dohodna sporočila so brezpogojno sprejeta od katerih koli gostiteljev v tem seznamu. Ti gostitelji se ne bodo preverjali po DNSBL seznamih in ne bodo dodani v listo sivih. Prejeta neželena pošta s teh gostiteljev ni nikoli zavrnjena, opcijsko pa se lahko premakne v mapo neželene pošte. Najpogostejša uporaba za to je navedba poštnih strežnikov, iz katerih ste nastavili pravilo za posredovanje pošte na vaš mailcow strežnik.", + "license_info": "Licenca ni zahtevana, a pomaga pri nadaljnjem razvoju.
    Registrirajte svoj GUID tukaj ali Kupite podporo za svojo namestitev Mailcow.", + "lookup_mx": "Cilj je regularni izraz, ki se ujema z imenom MX (.*\\.google\\.com za usmerjanje vse pošte, usmerjene na MX, ki se konča na google.com, prek tega skoka)", "main_name": "Naziv \"mailcow UI\"", "merged_vars_hint": "Sive vrstice so združene iz vars.(local.)inc.php in jih ni mogoče spremeniti.", - "oauth2_info": "OAuth2 implementacija omogoča grant vrste \"Authorization code\" in izdaja refresh tokene.
    \nStrežnik prav tako izda nove refresh tokene, ko je bil refresh token uporabljen

    \n• Privzeti obseg je profile. Samo uporabniki poštnih predalov se lahko prijavijo s pomočjo OAuth2. Če parameter obsega ni vnesen, se nastavi na profile.
    \n• Parameter state mora biti poslan s strani klienta kot del zahtevka za avtorizacijo .

    \nPoti za OAuth2 API:
    \n
      \n
    • Endpoint za avtorizacijo: /oauth/authorize
    • \n
    • Endpoint za tokene: /oauth/token
    • \n
    • Stran vira: /oauth/profile
    • \n
    \nPonovno generiranje client secret ne bo razveljavilo obstoječih avtorizacijskih kod, ne bodo pa mogle obnoviti svoje tokene.

    \nZavrnitev client tokenov bo povzročilo tekojčno prekinitev aktivnih sej. Vsi klienti se bodo morali ponovno prijaviti.", - "quarantine_redirect": "Preusmeri vsa obvestila k temu prejemniku:
    Pustite prazno, da onemogočite. Nepodpisana, nepreverjena pošta. Uporabljalo bi se naj samo za interno dostavo.", + "oauth2_info": "Implementacija OAuth2 podpira vrsto odobritve »Avtorizacijska koda« in izda osvežilne žetone.
    \nStrežnik samodejno izda tudi nove osvežilne žetone, ko je žeton za osvežitev uporabljen.

    \n• Privzeti obseg je profile. Prek OAuth2 je mogoče overiti samo uporabnike poštnega predala. Če parameter obsega izpustite, se vrne na profile.
    \n• Parameter state mora odjemalec poslati kot del zahteve za avtorizacijo.

    \nPoti za zahteve do API-ja OAuth2:
    \n
      \n
    • Končna točka avtorizacije: /oauth/authorize
    • \n
    • Končna točka žetona: /oauth/token
    • \n
    • Stran z viri: /oauth/profile
    • \n
    \nPonovno ustvarjanje skrivnosti odjemalca ne bo poteklo obstoječih kod za avtorizacijo, vendar ne bo obnovilo žetona.

    \nPreklic žetonov odjemalca bo povzročil takojšnjo prekinitev vseh aktivnih sej. Vse stranke se morajo ponovno overiti.", + "quarantine_redirect": "Preusmerite vsa obvestila temu prejemniku:
    Pustite prazno, če želite onemogočiti. Nepodpisana, nepreverjena pošta. Dostavljeno samo interno.", "quota_notification_html": "Predloga sporočila za obvestilo:
    Pustite prazno za obnovitev privzete predloge.", - "quota_notifications_vars": "{{percent}} pomeni trenutna omejitev uporabnika
    {{username}} je ime poštnega predala", - "r_info": "Sivi/onemogočeni elementi v seznamu aktivnih omejitev niso znane kot veljavne omejitve za mailcow in ne morejo biti premaknjene. Neznane omejitve bodo kljub temu nastavljene po vrstnem redu pojavitve.
    Nove elemente lahko dodate v inc/vars.local.inc.php da jih lahko vklopite ali izklopite.", - "relayhosts_hint": "Določite transporte glede na pošiljatelja, da jih lahko izberete v konfiguraciji domene.
    \nTransportni servis je vedno \"smtp:\" in bo poskušal s TLS ko bo na voljo. Wrapped TLS (SMTPS) ni podprto. Upošteva se uporabnikova politika odhodnega TLS.
    \nVpliva na izbrane domene vključno z alias domenami.", - "transport_dest_format": "Regex ali sintaksa: example.org, .example.org, *, box@example.org (več vrednosti ločite z vejico)", - "transport_test_rcpt_info": "• Uporabite null@hosted.mailcow.de za testiranje relaya na drugo destinacijo.", - "rspamd_global_filters_regex": "Njihovi nazivi pojasnijo njihov namen. Vsa vsebina mora imeti veljaven regular expression v obliki \"/pattern/options\" (npr. /.+@domain\\.tld/i).
    \nČeprav se v vsaki vrstici regexa izvedejo osnovni pregledi, je lahko funkcionalnost programa Rspamd motena, če sintaksa ni pravilna.
    \nRspamd bo poskušal prebrati vsebino preslikave, ko bo spremenjena. Če imate težave, ponovno zaženite Rspamd, da prisilite ponovno nalaganje preslikav.
    Elementi z Blackliste so izključeni iz karantene.", + "quota_notifications_vars": "{{percent}} je enako trenutni kvoti uporabnika
    {{username}} je ime poštnega predala", + "r_info": "Sivi/onemogočeni elementi v seznamu aktivnih omejitev niso znane kot veljavne omejitve za mailcow in ne morejo biti premaknjene. Neznane omejitve bodo kljub temu nastavljene po vrstnem redu pojavljanja.
    Nove elemente lahko dodate v inc/vars.local.inc.php da jih lahko vklopite ali izklopite.", + "relayhosts_hint": "Določite transporte, odvisne od pošiljatelja, da jih boste lahko izbrali v pogovornem oknu za konfiguracijo domen.
    \n Transportna storitev je vedno »smtp:« in bo zato poskusila s TLS, ko bo ponujena. Zaviti TLS (SMTPS) ni podprt. Upošteva se individualna nastavitev pravilnika za odhodni TLS uporabnika.
    \n Vpliva na izbrane domene, vključno z vzdevki domen.", + "transport_dest_format": "Regex ali sintaksa: example.org, .example.org, *, box@example.org (več vrednosti je lahko ločenih z vejicami)", + "transport_test_rcpt_info": "• Za preizkus posredovanja v tujino uporabite null@hosted.mailcow.de.", + "rspamd_global_filters_regex": "Njihova imena pojasnjujejo njihov namen. Vsa vsebina mora vsebovati veljaven regularni izraz v obliki »/vzorec/možnosti« (npr. /.+@domena\\.tld/i).
    \n Čeprav se v vsaki vrstici regularnega izraza izvajajo osnovna preverjanja, je lahko funkcionalnost Rspamdsa pokvarjena, če sintakse ne prebere pravilno.
    \n Rspamd bo poskušal prebrati vsebino zemljevida, ko se bo spremenila. Če pride do težav, znova zaženite Rspamd, da uveljavite ponovno nalaganje zemljevida.
    Elementi na seznamu zavrnjenih so izključeni iz karantene.", "rspamd_settings_map": "Preslikava nastavitev Rspamd", "sal_level": "Moo stopnja", "save": "Shrani spremembe", "search_domain_da": "Išči domene", "send": "Pošlji", "sender": "Pošiljatelj", - "service": "Servis", - "service_id": "ID servisa", + "service": "Storitev", + "service_id": "ID storitve", "source": "Vir", - "spamfilter": "Spam filter", + "spamfilter": "Filter neželene pošte", "subject": "Predmet", "success": "Uspešno", "sys_mails": "Sistemska pošta", @@ -326,7 +328,7 @@ "title_name": "Naziv spletnega mesta \"mailcow UI\"", "to_top": "Nazaj na vrh", "transport_maps": "Preslikave transportov", - "transports_hint": "• Vpis preslikave transporta nadredi preslikavo transporta odvisno od pošiljatelja.
    \n• Preferenčno se uporabljajo transporti glede na MX zapise.
    \n• Izhodne TLS politike na uporabnika so ignorirane in se lahko vsilijo samo s preslikavami TLS politik.
    \n• Transportni servis za definirane transporte je vedno \"smtp:\" in bo posledično poskušal TLS ko bo ponujeno. Wrapped TLS (SMTPS) ni podprto.
    \n• Naslovi, ki se ujemajo z \"/localhost$/\" bodo vedno preneseni preko \"local:\", in zato destinacija \"*\" ne bo vplivala na te naslove.
    \n• Za določitev poverilnic za naslednji skok (npr. \"[host]:25\"), Postfix vedno preveri \"host\" preden išče \"[host]:25\". Zaradi takšnega obnašanja je nemogoče hkrati uporabiti \"host\" in \"[host]:25\".", + "transports_hint": "• Vnos preslikave transporta preglasi preslikavo transporta, ki je odvisen od pošiljatelja.
    \n• Po možnosti se uporabljajo transporti, ki temeljijo na MX.
    \n• Nastavitve pravilnika TLS za odhodne uporabnike se prezrejo in jih je mogoče uveljaviti le z vnosi v zemljevidu pravilnika TLS.
    \n• Storitev transporta za definirane transporte je vedno »smtp:« in bo zato poskusila s TLS, ko bo ponujena. Zaviti TLS (SMTPS) ni podprt.
    \n• Naslovi, ki se ujemajo z \"/localhost$/\" bodo vedno preneseni preko \"local:\", in zato cilj \"*\" ne bo veljal za te naslove.
    \n• Za določitev poverilnic za zgledni naslednji skok \"[host]:25\", Postfix vedno poišče \"host\" preden poišče \"[host]:25\". Zaradi tega vedenja je nemogoče hkrati uporabljati \"host\" in \"[host]:25\".", "ui_footer": "Noga (HTML dovoljen)", "ui_header_announcement": "Obvestila", "ui_header_announcement_active": "Nastavi obvestilo kot aktivno", @@ -338,7 +340,7 @@ "ui_header_announcement_type_info": "Info", "ui_header_announcement_type_warning": "Pomembno", "ui_texts": "Oznake in besedila UI", - "unban_pending": "unban v postopku", + "unban_pending": "odblokada v teku", "unchanged_if_empty": "Če je nespremenjeno, pustite prazno", "upload": "Naloži", "username": "Uporabniško ime", @@ -352,13 +354,70 @@ "allowed_origins": "Upravljanje-dostopa-Dovoljeni-Viri", "copy_to_clipboard": "Besedilo kopirano v odložišče!", "f2b_manage_external": "Zunanje upravljanje Fail2Ban", - "f2b_manage_external_info": "Fail2ban bo še vedno vzdrževal seznam prepovedi, vendar ne bo aktivno nastavil pravil za blokiranje prometa. Uporabite spodnji ustvarjeni seznam prepovedi za zunanje blokiranje prometa." + "f2b_manage_external_info": "Fail2ban bo še vedno vzdrževal seznam prepovedi, vendar ne bo aktivno nastavil pravil za blokiranje prometa. Uporabite spodnji ustvarjeni seznam prepovedi za zunanje blokiranje prometa.", + "force_sso_text": "Če je konfiguriran zunanji ponudnik OIDC, ta možnost skrije privzete obrazce za prijavo v Mailcow in prikaže samo gumb za enotno prijavo", + "app_hide": "Skrij za prijavo", + "iam_login_provisioning": "Samodejno ustvarjanje uporabnikov ob prijavi", + "admin_quicklink": "Skrij hitro povezavo do strani za prijavo skrbnika", + "login_page": "Prijavna stran", + "domainadmin_quicklink": "Skrij hitro povezavo do strani za prijavo v Skrbnik domen", + "filter": "Filter", + "force_sso": "Onemogoči prijavo v mailcow in prikaži samo enotno prijavo", + "iam": "Ponudnik identitete", + "iam_attribute_field": "Polje atributa", + "iam_authorize_url": "Končna točka avtorizacije", + "iam_auth_flow": "Potek preverjanja pristnosti", + "iam_auth_flow_info": "Poleg toka avtorizacijske kode (standardni tok v Keycloaku), ki se uporablja za prijavo z enotno prijavo, mailcow podpira tudi tok preverjanja pristnosti z neposrednimi poverilnicami. Tok Mailpassword poskuša preveriti uporabnikove poverilnice z uporabo REST API-ja Keycloak Admin. mailcow pridobi zgoščeno geslo iz atributa mailcow_password, ki je preslikan v Keycloaku.", + "iam_basedn": "Osnovni DN", + "iam_client_id": "ID odjemalca", + "iam_client_secret": "Skrivnost odjemalca", + "iam_client_scopes": "Obsegi odjemalcev", + "iam_default_template": "Privzeta predloga", + "iam_default_template_description": "Če uporabniku ni dodeljena nobena predloga, bo privzeta predloga uporabljena za ustvarjanje nabiralnika, ne pa za posodabljanje nabiralnika.", + "iam_description": "Konfigurirajte zunanjega ponudnika za preverjanje pristnosti
    Uporabnikovi poštni nabiralniki bodo samodejno ustvarjeni ob prvi prijavi, če je nastavljeno preslikavanje atributov.", + "iam_extra_permission": "Za delovanje naslednjih nastavitev potrebuje odjemalec mailcow v Keycloaku Servisni račun in dovoljenje za ogled uporabnikov.", + "iam_host": "Gostitelj", + "iam_host_info": "Vnesite enega ali več gostiteljev LDAP, ločenih z vejicami.", + "iam_import_users": "Uvozi uporabnike", + "iam_use_tls_info": "Če omogočite TLS, morate uporabiti privzeta vrata za strežnik LDAP (389). Vrat SSL ni mogoče uporabiti.", + "iam_sync_interval": "Interval sinhronizacije / uvoza (min)", + "user_quicklink": "Skrij hitro povezavo do strani za prijavo uporabnika", + "iam_use_ssl_info": "Če omogočite SSL in so vrata nastavljena na 389, bodo samodejno prepisana na uporabo 636.", + "iam_mapping": "Preslikava atributov", + "iam_bindpass": "Vezano geslo", + "iam_periodic_full_sync": "Periodična popolna sinhronizacija", + "iam_port": "Vrata", + "iam_redirect_url": "Preusmeri URL", + "iam_rest_flow": "Tok gesla za pošto", + "iam_server_url": "URL strežnika", + "iam_sso": "Enotna prijava", + "iam_test_connection": "Preizkus povezave", + "iam_token_url": "Končna točka žetona", + "iam_userinfo_url": "Končna točka z uporabniškimi podatki", + "iam_username_field": "Polje z uporabniškim imenom", + "iam_binddn": "Vezava DN", + "iam_use_ssl": "Uporabi SSL", + "iam_use_tls": "Uporabi StartTLS", + "iam_version": "Različica", + "ignore_ssl_error": "Prezri napake SSL", + "password_reset_info": "Če ni naveden e-poštni naslov za obnovitev, te funkcije ni mogoče uporabiti.", + "password_reset_settings": "Nastavitve za obnovitev gesla", + "password_reset_tmpl_html": "Predloga HTML", + "password_reset_tmpl_text": "Predloga besedila", + "password_settings": "Nastavitve gesla", + "quicklink_text": "Prikaži ali skrij hitre povezave do drugih strani za prijavo pod prijavnim obrazcem", + "reset_password_vars": "{{link}} Ustvarjena povezava za ponastavitev gesla
    {{username}} Ime nabiralnika uporabnika, ki je zahteval ponastavitev gesla
    {{username2}} Ime obnovitvenega nabiralnika
    {{date}} Datum zahteve za ponastavitev gesla
    {{token_lifetime}} Življenjska doba žetona v minutah
    {{hostname}} Ime gostitelja mailcow", + "restore_template": "Za obnovitev privzete predloge pustite polje prazno.", + "task": "Naloga", + "user_link": "Uporabniška povezava", + "iam_realm": "Realm", + "needs_restart": "potreben je ponovni zagon" }, "danger": { - "alias_goto_identical": "Alias in goto naslov morata biti identična", - "aliasd_targetd_identical": "Alias domena ne sme biti enaka ciljni domeni: %s", - "bcc_exists": "BCC preslikava obstaja za vrsto %s", - "dkim_domain_or_sel_exists": "DKIM ključ za \"%s\" obstaja in ne bo prepisan.", + "alias_goto_identical": "Vzdevek in ciljni naslov se ne smeta ujemati", + "aliasd_targetd_identical": "Vzdevek domene ne sme biti enak ciljni domeni: %s", + "bcc_exists": "Za tip %s obstaja BCC preslikava %s", + "dkim_domain_or_sel_exists": "DKIM ključ za \"%s\" obstaja in ne bo prepisan", "domain_quota_m_in_use": "Kvota domene mora biti večja ali enaka %s MiB", "extra_acl_invalid_domain": "Zunanji pošiljatelj \"%s\" uporablja neveljavno domeno", "global_map_write_error": "Ni mogoče zapisati ID globalne preslikave %s: %s", @@ -366,71 +425,71 @@ "invalid_nexthop": "Oblika naslednjega skoka ni veljavna", "invalid_nexthop_authenticated": "Naslednji skok obstaja z drugačnimi poverilnicami. Prosim najprej posodobite obstoječe poverilnice za ta naslednji skok.", "demo_mode_enabled": "Demo način je omogočen", - "access_denied": "Dostop zavrnjen ali pa so podatki obrazca napačni", - "alias_domain_invalid": "Alias domena %s ni veljavna", - "alias_empty": "Alias naslov ne sme biti prazen", - "alias_invalid": "Alias naslov %s ni veljaven", - "aliases_in_use": "Max. aliasov mora biti večje ali enako %d", - "app_name_empty": "Naziv aplikacije ne more biti prazno", - "app_passwd_id_invalid": "ID gesla aplikacije %s je neveljaven", - "bcc_empty": "BCC cilj ne more biti prazen", - "bcc_must_be_email": "BCC cilj %s ni veljaven e-poštni naslov", - "comment_too_long": "Komentar je predolg, dovoljeno je največ 100 znakov.", - "defquota_empty": "Privzeta kvota na poštni predal ne more biti 0", - "description_invalid": "Opis resursa za %s ni veljaven", + "access_denied": "Dostop zavrnjen ali neveljavni podatki obrazca", + "alias_domain_invalid": "Vzdevek domene %s ni veljaven", + "alias_empty": "Naslov vzdevka ne sme biti prazen", + "alias_invalid": "Naslov vzdevka %s ni veljaven", + "aliases_in_use": "Največje število vzdevkov mora biti večje ali enako %d", + "app_name_empty": "Ime aplikacije ne sme biti prazno", + "app_passwd_id_invalid": "ID gesla za aplikacijo %s neveljaven", + "bcc_empty": "Polje za prejemnika BCC ne sme biti prazno", + "bcc_must_be_email": "Cilj BCC %s ni veljaven e-poštni naslov", + "comment_too_long": "Komentar je predolg, dovoljeno je največ 160 znakov", + "defquota_empty": "Privzeta kvota na poštni predal ne more biti 0.", + "description_invalid": "Opis vira za %s je neveljaven", "dkim_domain_or_sel_invalid": "Domena ali izbirnik DKIM ni veljaven: %s", "domain_cannot_match_hostname": "Domena se ne more ujemati z imenom gostitelja", "domain_exists": "Domena %s že obstaja", - "domain_invalid": "Manjka ali napačno ime domene", - "domain_not_empty": "Ne morem odstraniti ne-prazno domeno %s", + "domain_invalid": "Ime domene je prazno ali neveljavno", + "domain_not_empty": "Neprazne domene %s ni mogoče odstraniti", "domain_not_found": "Domene %s ni bilo mogoče najti", "extended_sender_acl_denied": "manjka ACL za določitev naslovov zunanjih pošiljateljev", "extra_acl_invalid": "Naslov zunanjega pošiljatelja \"%s\" ni veljaven", "fido2_verification_failed": "Preverjanje FIDO2 ni uspelo: %s", - "file_open_error": "Datoteka ne more biti odprta za urejanje", + "file_open_error": "Datoteke ni mogoče odpreti za pisanje", "filter_type": "Napačna vrsta filtra", - "from_invalid": "Pošiljatelj ne sme biti prazno", + "from_invalid": "Polje za pošiljatelja ne sme biti prazno", "global_filter_write_error": "Ni mogoče zapisati datoteke filtra: %s", "global_map_invalid": "ID globalne preslikave %s ni veljaven", - "goto_empty": "Alias naslov mora vsebovati vsaj en veljaven goto naslov", - "goto_invalid": "Goto naslov %s ni veljaven", + "goto_empty": "Naslov vzdevka mora vsebovati vsaj en veljaven ciljni naslov", + "goto_invalid": "Ciljni naslov %s ni veljaven", "ham_learn_error": "Napaka pri učenju Ham: %s", - "imagick_exception": "Napaka: Imagick napaka pri branju slike", + "imagick_exception": "Napaka: Izjema Imagick med branjem slike", "img_invalid": "Ni možno preveriti slikovne datoteke", "invalid_bcc_map_type": "Neveljavna vrsta preslikave BCC", "invalid_destination": "Ciljna oblika \"%s\" ni veljavna", "invalid_filter_type": "Neveljavna vrsta filtra", "invalid_host": "Naveden je neveljaven gostitelj (host): %s", - "invalid_mime_type": "Neveljaven mime type", + "invalid_mime_type": "Neveljavna vrsta MIME", "max_quota_in_use": "Kvota poštnega predala mora biti večja ali enaka %d MB", "password_complexity": "Geslo ne ustreza varnostni politiki", - "pushover_credentials_missing": "Manjka Pushover token ali ključ", + "pushover_credentials_missing": "Manjka žeton in/ali ključ Pushover", "release_send_failed": "Sporočila ni bilo mogoče sprostiti: %s", - "tls_policy_map_dest_invalid": "Cilj politike ni veljaven", + "tls_policy_map_dest_invalid": "Cilj pravilnika je neveljaven", "webauthn_authenticator_failed": "Izbrani avtentikator ni bil najden", "reset_f2b_regex": "Regex filter ni bilo možno ponastaviti v ustreznem času. Prosim poskusite ponovno ali počakajte nekaj sekund in ponovno naložite stran.", "target_domain_invalid": "Ciljna domena %s ni veljavna", - "validity_missing": "Prosim nastavite obdobje veljavnosti", + "validity_missing": "Prosim določite obdobje veljavnosti", "invalid_recipient_map_old": "Naveden neveljaven izvirni prejemnik: %s", - "ip_list_empty": "Seznam dovoljenih IPjev ne sme biti prazen", - "is_alias": "%s je že znan kot alias naslov", - "is_alias_or_mailbox": "%s je že znan kot alias, poštni naslov, ali alias izveden iz alias domene", - "is_spam_alias": "%s že obstaja kot začasen alias (spam alias naslov)", - "last_key": "Zadnji ključ ne more biti izbrisan, prosim raje deaktivirajte dvofaktorsko avtentikacijo (TFA)", + "ip_list_empty": "Seznam dovoljenih IP-jev ne sme biti prazen", + "is_alias": "%s je že znan kot naslov vzdevka", + "is_alias_or_mailbox": "%s je že znan kot vzdevek, poštni predal ali naslov vzdevka, razširjen iz vzdevka domene.", + "is_spam_alias": "%s je že znan kot začasni vzdevek (neželeni vzdevek)", + "last_key": "Zadnji ključ ne more biti izbrisan, prosim raje deaktivirajte dvofaktorsko avtentikacijo (TFA).", "login_failed": "Prijava ni uspela", "mailbox_defquota_exceeds_mailbox_maxquota": "Privzeta kvota presega najvišjo omejitev", "mailbox_invalid": "Ime poštnega predala ni veljavno", - "mailbox_quota_exceeded": "Kvota presega omejitev domene (maksimalno %d MB)", + "mailbox_quota_exceeded": "Kvota presega omejitev domene (največ %d MB)", "mailbox_quota_exceeds_domain_quota": "Najvišja kvota presega omejitev domene", "mailbox_quota_left_exceeded": "Ni dovolj prostora (preostali prostor: %d MB)", "mailboxes_in_use": "Največje število poštnih predalov mora biti večje ali enako %d", "malformed_username": "Nepravilno oblikovano uporabniško ime", "map_content_empty": "Preslikava vsebine ne more biti prazna", - "max_alias_exceeded": "Preseženo največje število aliasov", + "max_alias_exceeded": "Preseženo največje število vzdevkov", "max_mailbox_exceeded": "Preseženo največje število poštnih predalov (%d od %d)", - "maxquota_empty": "Največja kvota na poštni predal ne more biti 0", + "maxquota_empty": "Največja kvota na poštni predal ne more biti 0.", "mysql_error": "Napaka MySQL: %s", - "network_host_invalid": "Nepravilno omrežje ali gostitel: %s", + "network_host_invalid": "Nepravilno omrežje ali gostitelj: %s", "next_hop_interferes": "% moti naslednji skok %s", "next_hop_interferes_any": "Obstoječi naslednji skok moti %s", "nginx_reload_failed": "Ponovni zagon Nginx ni uspel: %s", @@ -443,30 +502,30 @@ "policy_list_from_invalid": "Zapis ima nepravilno obliko", "private_key_error": "Napaka zasebnega ključa: %s", "pushover_key": "Pushover ključ ni v pravilni obliki", - "pushover_token": "Pushover token ni v pravilni obliki", - "quota_not_0_not_numeric": "Quota mora biti število in večje ali enako 0", + "pushover_token": "Pushover žeton ni v pravilni obliki", + "quota_not_0_not_numeric": "Kvota mora biti numerična in >= 0", "recipient_map_entry_exists": "Preslikava prejemnika \"%s\" že obstaja", "redis_error": "Napaka Redis: %s", "relayhost_invalid": "Vnos preslikave %s ni pravilen", - "resource_invalid": "Ime vira je neveljavno", - "rl_timeframe": "Časovni okvir za rate limit je nepravilen", + "resource_invalid": "Ime vira %s je neveljavno", + "rl_timeframe": "Časovni okvir omejitve je nepravilen", "rspamd_ui_pw_length": "Rspamd UI geslo mora biti dolgo vsaj 6 znakov", - "script_empty": "Script ne more biti prazen", + "script_empty": "Skripta ne sme biti prazna", "sender_acl_invalid": "Vrednost ACL pošiljatelja %s ni veljavna", "set_acl_failed": "Ni uspelo nastaviti ACL", "settings_map_invalid": "ID preslikave nastavitev %s ni veljaven", - "sieve_error": "Napaka Sieve parserja: %s", - "spam_learn_error": "Napaka pri učenju spama: %s", - "subject_empty": "Predmet ne sme biti prazno", + "sieve_error": "Napaka Sieve razčlenjevalnika: %s", + "spam_learn_error": "Napaka pri učenju neželene pošte: %s", + "subject_empty": "Zadeva ne sme biti prazna", "targetd_not_found": "Ciljna domena %s ni bila najdena", - "targetd_relay_domain": "Ciljna domena %s je relay domena", + "targetd_relay_domain": "Ciljna domena %s je posredovalna domena", "template_exists": "Predloga %s že obstaja", "template_id_invalid": "ID predloge %s ni veljaven", "template_name_invalid": "Ime predloge ni veljavno", "text_empty": "Besedilo ne sme biti prazno", - "tfa_token_invalid": "Neveljaven token TFA", - "tls_policy_map_entry_exists": "Vpis preslikave TLS \"%s\" že obstaja", - "tls_policy_map_parameter_invalid": "Parameter politike ni pravilen", + "tfa_token_invalid": "Neveljaven TFA žeton", + "tls_policy_map_entry_exists": "Vnos pravilnika preslikave TLS \"%s\" obstaja", + "tls_policy_map_parameter_invalid": "Parameter pravilnika je neveljaven", "totp_verification_failed": "Neuspešno preverjanje TOTP", "transport_dest_exists": "Cilj transporta \"%s\" že obstaja", "webauthn_verification_failed": "Preverjanje WebAuthn ni uspelo: %s", @@ -483,10 +542,24 @@ "cors_invalid_origin": "Naveden neveljaven Allow-Origin", "invalid_recipient_map_new": "Naveden neveljaven nov prejemnik: %s", "img_dimensions_exceeded": "Slika presega največje dovoljene dimenzije", - "img_size_exceeded": "Slika presega največjo dovoljeno velikost datoteke" + "img_size_exceeded": "Slika presega največjo dovoljeno velikost datoteke", + "iam_test_connection": "Povezava ni uspela", + "authsource_in_use": "Ponudnika identitete ni mogoče spremeniti ali izbrisati, ker ga trenutno uporablja eden ali več uporabnikov.", + "password_reset_na": "Obnovitev gesla trenutno ni na voljo. Obrnite se na skrbnika.", + "generic_server_error": "Prišlo je do nepričakovane napake strežnika. Obrnite se na skrbnika.", + "invalid_reset_token": "Neveljaven žeton za ponastavitev", + "password_reset_invalid_user": "Poštni predal ni bil najden ali pa ni nastavljen e-poštni naslov za obnovitev", + "recovery_email_failed": "E-poštnega sporočila za obnovitev ni bilo mogoče poslati. Obrnite se na skrbnika.", + "required_data_missing": "Manjkajo zahtevani podatki %s", + "reset_token_limit_exceeded": "Omejitev žetonov za ponastavitev je bila presežena. Poskusite znova pozneje.", + "to_invalid": "Polje za prejemnika ne sme biti prazno", + "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" }, "debug": { - "containers_info": "Informacije o vsebniku (containerju)", + "containers_info": "Informacije o zabojniku", "architecture": "Arhitektura", "chart_this_server": "Diagram (ta strežnik)", "container_running": "Aktiven", @@ -500,23 +573,23 @@ "external_logs": "Zunanji dnevniki", "last_modified": "Nazadnje spremenjeno", "history_all_servers": "Zgodovina (vsi strežniki)", - "in_memory_logs": "In-memory dnevniki", - "service": "Servis", + "in_memory_logs": "Dnevniki v pomnilniku", + "service": "Storitev", "show_ip": "Prikaži javni IP", "size": "Velikost", "started_at": "Zagnano ob", "started_on": "Zagnano na", "static_logs": "Statični dnevniki", "success": "Uspešno", - "system_containers": "Sistem in Containerji", + "system_containers": "Sistem in zabojniki", "timezone": "Časovni pas", "uptime": "Čas delovanja", "update_available": "Posodobitev je na voljo", "no_update_available": "Sistem je na najnovejši verziji", "update_failed": "Ni mogoče preveriti za posodobitve", "username": "Uporabniško ime", - "wip": "Trenutno v delu", - "log_info": "

    mailcow in-memory dnevniki se zbirajo v Redis seznamih in se vsako minuto omejijo na LOG_LINES (%d) da se zmanjša obremenitev.\n
    In-memory dnevniki niso namenjeni trajnemu shranjevanju. Vse aplikacije, ki beležijo dnevnike in-memory, tudi beležijo v Docker daemon in posledično v privzeti gonilnik za dnevnik.\n
    In-memory dnevniki se naj uporabljajo za odpravljanje manjših napak s containerji.

    \n

    Eksterni dnevniki se zbirajo preko API-ja posamezne aplikacije.

    \n

    Statični dnevniki so večinoma dnevniki aktivnosti, ki se ne beležijo v Dockerd, a jih je vseeno treba hraniti (razen API dnevnikov).

    ", + "wip": "Trenutno delo v teku", + "log_info": "

    Dnevniki v pomnilniku mailcow se zbirajo na seznamih Redis in vsako minuto skrajšajo na LOG_LINES (%d), da se zmanjša preobremenitev.\n
    Dnevniki v pomnilniku niso namenjeni trajnemu beleženju. Vse aplikacije, ki se beležijo v pomnilnik, se beležijo tudi v Dockerjev demon in s tem v privzeti gonilnik beleženja.

    \n

    Vrsta dnevnika v pomnilniku se mora uporabljati za odpravljanje manjših težav s kontejnerji.

    \n

    Zunanji dnevniki se zbirajo prek API-ja dane aplikacije.

    \n

    Statični dnevniki so večinoma dnevniki dejavnosti, ki se ne beležijo v Dockerd, vendar morajo biti še vedno trajni (razen dnevnikov API-ja).

    ", "login_time": "Čas", "logs": "Dnevniki", "memory": "Spomin", @@ -527,7 +600,7 @@ "infoFiltered": "(filtrirano od _MAX_ skupaj zapisov)", "collapse_all": "Strni vse", "decimal": ",", - "emptyTable": "Ni podatkov", + "emptyTable": "V tabeli ni na voljo podatkov", "expand_all": "Razširi vse", "info": "Prikazano _START_ do _END_ od _TOTAL_ zapisov", "infoEmpty": "Prikazano 0 do 0 od 0 zapisov", @@ -549,9 +622,9 @@ } }, "diagnostics": { - "cname_from_a": "Vrednost pridobljena iz A/AAAA zapisa. To je podprto, če zapis kaže na pravilen resurs.", + "cname_from_a": "Vrednost, izpeljana iz zapisa A/AAAA. To je podprto, če zapis kaže na pravilen vir.", "dns_records": "DNS zapisi", - "dns_records_24hours": "Prosim upoštevajte, da lahko traja do 24 ur da se spremembe v DNS pravilno prikažejo na tej strani. Namen je da lahko enostavno vidite, kako konfigurirati svoje DNS zapise in preverite ali so vaši zapisi pravilno shranjeni v DNS.", + "dns_records_24hours": "Upoštevajte, da se lahko spremembe DNS-a pravilno odražajo na tej strani v 24 urah. Namenjena je temu, da si preprosto ogledate, kako konfigurirati zapise DNS, in preverite, ali so vsi vaši zapisi pravilno shranjeni v DNS-u.", "dns_records_data": "Pravilni podatki", "dns_records_docs": "Prosim preverite tudi dokumentacijo.", "dns_records_name": "Ime", @@ -562,60 +635,60 @@ "edit": { "acl": "ACL (Dovoljenje)", "active": "Aktivno", - "allow_from_smtp": "Dovoli samo tem IP naslovom da uporabijo SMTP", - "bcc_dest_format": "Cilj BCC mora biti en veljaven email naslov.
    Če morate poslati kopijo na več naslovov, ustvarite alias in ga uporabite tukaj.", - "automap": "Poskušaj samodejno preslikati mape (\"Sent items\", \"Sent\" => \"Poslano\" ipd.)", + "allow_from_smtp": "Dovoli samo tem IP naslovom uporabo SMTP", + "bcc_dest_format": "Ciljna stran za polje SKP (BCC) mora biti en veljaven e-poštni naslov.
    Če morate kopijo poslati na več naslovov, ustvarite vzdevek in ga uporabite tukaj.", + "automap": "Poskusite samodejno preslikati mape (\"Poslani predmeti\", \"Poslano\" => \"Poslano\" itd.)", "admin": "Uredi skrbnika", "domain_footer_info_vars": { - "custom": "{= foo =} - Če ima poštni predal atribut po meri \"foo\" z vrednostjo \"bar\", spremenljivka vrne \"bar\"", - "auth_user": "{= auth_user =} - Prijavljeno uporabniško ime, ki ga določi MTA", - "from_user": "{= from_user =} - leva stran email naslova uporabnika, npr. za \"moo@mailcow.tld\" vrne \"moo\"", - "from_name": "{= from_name =} - Prikazno ime, npr. za \"Mailcow <moo@mailcow.tld>\" vrne \"Mailcow\"", - "from_addr": "{= from_addr =} - e-poštni naslov \"Od\"", - "from_domain": "{= from_domain =} - domena e-poštnega naslova \"Od\"" + "custom": "{= foo =} - Če ima poštni predal atribut po meri \"foo\" z vrednostjo \"bar\", vrne \"bar\"", + "auth_user": "{= auth_user =} - Preverjeno uporabniško ime, ki ga določi MTA", + "from_user": "{= from_user =} - Iz uporabniškega dela ovojnice, npr. za \"moo@mailcow.tld\" vrne \"moo\"", + "from_name": "{= from_name =} - Iz imena ovojnice, npr. za \"Mailcow <moo@mailcow.tld>\" vrne \"Mailcow\"", + "from_addr": "{= from_addr =} - Del ovojnice z naslovom od", + "from_domain": "{= from_domain =} - Iz domenskega dela ovojnice" }, - "dont_check_sender_acl": "Onemogoči kontrolo pošiljatelja za domeno %s (+ alias domene)", + "dont_check_sender_acl": "Onemogoči preverjanje pošiljatelja za domeno %s (+ vzdevki domen)", "pushover_title": "Naslov obvestila", "domains": "Domene", - "extended_sender_acl_info": "Če je DKIM domenski ključ na voljo, ga uvozite.
    \n Ne pozabite dodati ta strežnik k ustreznemu SPF TXT zapisu.
    \n Kadar koli je domena ali alias domena dodana k tem strežniku, ki se prekriva z zunanjim naslovom, je zunanji naslov odstranjen.
    \n uporabite @domain.tld da dovolite pošiljanje kot *@domain.tld.", - "lookup_mx": "Cilj je regular expression za ujemanje MX zapisov (.*\\.google\\.com za usmeritev vse pošte na MX, ki se konča z google.com, preko tega skoka)", - "maxbytespersecond": "Največ bytov na sekundo
    (0 = neomejeno)", + "extended_sender_acl_info": "Uvoziti je treba ključ domene DKIM, če je na voljo.
    \n Ne pozabite dodati tega strežnika v ustrezni zapis SPF TXT.
    \n Kadar koli je temu strežniku dodana domena ali vzdevek domene, ki se prekriva z zunanjim naslovom, se zunanji naslov odstrani.
    \n Uporabite @domain.tld, da omogočite pošiljanje kot *@domain.tld.", + "lookup_mx": "Cilj je regularni izraz, ki se ujema z imenom MX (.*\\.google\\.com za usmerjanje vse pošte, usmerjene na MX, ki se konča na google.com, prek tega skoka)", + "maxbytespersecond": "Največ bajtov na sekundo
    (0 = neomejeno)", "pushover_sender_array": "Upoštevaj samo sledeče e-poštne naslove pošiljateljev (ločeni z vejico)", - "mbox_rl_info": "Ta omejitev velja za SASL uporabniško ime, preverja se ujemanje s katerim koli \"from\" naslovom, ki ga uporablja prijavljeni uporabnik. Omejitev pošiljanja za poštni predal preglasi pravilo omejitve za domeno.", + "mbox_rl_info": "Ta omejitev se uporabi za prijavno ime SASL in se ujema z naslovom \"od\", ki ga uporablja prijavljeni uporabnik. Omejitev poštnega nabiralnika preglasi omejitev za celotno domeno.", "kind": "Tip", - "client_secret": "Client secret", - "comment_info": "Zasebni komentar ni viden uporabniku, javni komentar pa je viden kot tooltip v uporabnikovem pregledu.", + "client_secret": "Skrivnost odjemalca", + "comment_info": "Zasebni komentar ni viden uporabniku, javni komentar pa se prikaže kot opis orodja, ko nanj v pregledu uporabnika zadržite miško", "created_on": "Ustvarjeno", "custom_attributes": "Atributi po meri", - "delete1": "Izbriši na viru, ko je končano", - "delete2": "Izbriši sporočila na cilju, ki ne obstajajo na viru", + "delete1": "Izbriši iz vira, ko je končano", + "delete2": "Izbriši sporočila na cilju, ki niso na izvoru", "delete2duplicates": "Izbriši dvojnike na cilju", "delete_ays": "Prosim potrdite proces izbrisa.", "description": "Opis", "disable_login": "Onemogoči prijavo (dohodna pošta je še vedno sprejeta)", "domain": "Uredi domeno", - "domain_admin": "Uredi domenskega skrbnika", + "domain_admin": "Uredi skrbnika domene", "domain_footer": "Noga za celo domeno", "domain_footer_html": "HTML noga", - "pushover_vars": "Če ni definiran noben filter pošiljatelja, bodo upoštevana vsa sporočila.
    Regex filtre in natančna preverjanja pošiljateljev je mogoče definirati posamezno in bodo obravnavani v nadaljevanju. Niso odvisni drug od drugega.
    Uporabne spremenljivke za besedilo in naslov (prosimo, upoštevajte politike varstva podatkov)", + "pushover_vars": "Če filter pošiljatelja ni definiran, bodo upoštevana vsa e-poštna sporočila.
    Filtre regularnih izrazov in natančna preverjanja pošiljateljev je mogoče definirati posamično in bodo obravnavana zaporedno. Niso odvisna drug od drugega.
    Uporabne spremenljivke za besedilo in naslov (upoštevajte pravilnike o varstvu podatkov)", "pushover_verify": "Preveri poverilnice", "quota_mb": "Omejitev (MiB)", - "quota_warning_bcc": "BCC za sporočilo z opozorilom omejitve", - "quota_warning_bcc_info": "Opozorila bodo poslana kot ločene kopije sledečim prejemnikom. K naslovu sporočila bo dodano uporabniško ime v oklepajih, npr. Opozorilo omejitve (user@example.com)", + "quota_warning_bcc": "Opozorilo o kvoti BCC", + "quota_warning_bcc_info": "Opozorila bodo poslana kot ločene kopije naslednjim prejemnikom. Zadevi bo v oklepaju dodano ustrezno uporabniško ime, na primer: Opozorilo o kvoti (uporabnik@example.com).", "ratelimit": "Omejitev pošiljanja", "advanced_settings": "Napredne nastavitve", - "allow_from_smtp_info": "Pustite prazno da dovolite vse pošiljatelje.
    IPv4/IPv6 naslovi in omrežja.", - "allowed_protocols": "Dovoljeni protokoli", + "allow_from_smtp_info": "Pustite prazno, da dovolite vse pošiljatelje.
    Naslovi in omrežja IPv4/IPv6.", + "allowed_protocols": "Dovoljeni protokoli za neposreden dostop uporabnikov (ne vpliva na protokole za gesla aplikacij)", "app_name": "Ime aplikacije", "app_passwd": "Geslo aplikacije", "app_passwd_protocols": "Dovoljeni protokoli za geslo aplikacije", - "backup_mx_options": "Možnosti posredovanja (relay)", - "client_id": "Client ID", - "domain_footer_info": "Noge za celo domeno so dodane k vsem izhodnim e-poštnim sporočilom v tej domeni.
    V nogi se lahko uporabijo sledeče spremenljivke:", - "domain_footer_plain": "PLAIN noga", - "domain_footer_skip_replies": "Ne dodajaj noge v odgovorih na e-poštna sporočila", - "domain_quota": "Omejitev (kvota) domene", - "edit_alias_domain": "Uredi alias domeno", + "backup_mx_options": "Možnosti posredovanja", + "client_id": "ID odjemalca", + "domain_footer_info": "Noge za celotno domeno so dodane vsem odhodnim e-poštnim sporočilom, povezanim z naslovom znotraj te domene.
    Za nogo se lahko uporabijo naslednje spremenljivke:", + "domain_footer_plain": "NAVADNA noga", + "domain_footer_skip_replies": "Prezri nogo v odgovorih na e-poštna sporočila", + "domain_quota": "Kvota domene", + "edit_alias_domain": "Uredi vzdevek domene", "exclude": "Izključi objekte (regex)", "extended_sender_acl": "Naslovi zunanjih pošiljateljev", "force_pw_update": "Obvezna zamenjava gesla ob naslednji prijavi", @@ -624,18 +697,18 @@ "full_name": "Polno ime", "gal": "Globalni seznam naslovov (GAL)", "gal_info": "GAL vsebuje vse objekte v domeni in jih uporabniki ne morejo urejati. Če je onemogočeno, ni podatkov o o zasedenosti objekta! Ponovno zaženite SOGo za uveljavitev sprememb.", - "generate": "generiraj", + "generate": "ustvari", "grant_types": "Vrste dovoljenj", "hostname": "Ime gostitelja", "inactive": "Neaktivno", "last_modified": "Nazadnje spremenjeno", "mailbox": "Uredi poštni predal", - "mailbox_quota_def": "Privzeta omejitev/kvota za poštni predal", - "mailbox_relayhost_info": "Velja samo za poštni predal in neposredne aliase. Ne prepiše domenskega relay gostitelja.", - "max_aliases": "Največ aliasov", - "max_mailboxes": "Največ možnih poštnih predalov", - "max_quota": "Največja omejitev/kvota na poštni predal (MiB)", - "maxage": "Največja starost sporočil (v dnevih), po katerih bo poizvedeno iz oddaljenega vira
    (0 = ne omejuj)", + "mailbox_quota_def": "Privzeta kvota nabiralnika", + "mailbox_relayhost_info": "Uporablja se samo za poštni nabiralnik in neposredne vzdevke, preglasi gostitelja posredovalne domene.", + "max_aliases": "Največje število vzdevkov", + "max_mailboxes": "Največje možno število poštnih predalov", + "max_quota": "Največja kvota na poštni predal (MiB)", + "maxage": "Najvišja starost sporočil v dnevih, ki bodo prebrana z oddaljenega strežnika
    (0 = prezri starost)", "mins_interval": "Interval (min)", "multiple_bookings": "Več rezervacij", "none_inherit": "Brez / podeduj", @@ -647,12 +720,709 @@ "public_comment": "Javni komentar", "pushover": "Pushover", "pushover_evaluate_x_prio": "Eskaliraj visoko prednostno pošto [X-Priority: 1]", - "pushover_info": "Nastavitve potisnih obvestil bodo veljala za vsa čisto (ne spam) elektronsko pošto dostavljeno v %s vključno z aliasi (deljeni, nedeljeni, označeni)", + "pushover_info": "Nastavitve potisnih obvestil bodo veljale za vso čisto (ne neželeno) pošto, dostavljeno na %s, vključno z vzdevki (v skupni rabi, brez skupne rabe, označeni).", "pushover_only_x_prio": "Upoštevaj samo pošto z visoko prioriteto [X-Priority: 1]", "pushover_sender_regex": "Upoštevaj sledeči regex za pošiljatelja", "pushover_text": "Besedilo obvestila", "pushover_sound": "Zvok", "encryption": "Šifriranje", - "alias": "Uredi alias" + "alias": "Uredi vzdevek", + "relayhost": "Prenosi, odvisni od pošiljatelja", + "mailbox_rename_alias": "Samodejno ustvari vzdevek", + "sender_acl_info": "Če lahko uporabnik poštnega predala A pošilja kot uporabnik poštnega predala B, se naslov pošiljatelja v SOGo ne prikaže samodejno kot izbirno polje \"od\".
    \n Uporabnik poštnega predala B mora v SOGo ustvariti pooblastilo, da lahko uporabnik poštnega predala A izbere svoj naslov kot pošiljatelja. Če želite pooblastiti poštni predal v SOGo, uporabite meni (tri pike) desno od imena vašega poštnega predala v zgornjem levem kotu v pogledu pošte. To vedenje ne velja za vzdevke.", + "redirect_uri": "URL za preusmeritev/povratni klic", + "relay_all": "Posreduj vsem prejemnikom", + "relay_unknown_only": "Posreduj samo neobstoječe poštne nabiralnike. Obstoječi poštni nabiralniki bodo dostavljeni lokalno.", + "relay_domain": "Posreduj to domeno", + "remove": "Odstrani", + "resource": "Vir", + "mailbox_rename_title": "Novo ime lokalnega poštnega predala", + "password_recovery_email": "E-poštno sporočilo za obnovitev gesla", + "relay_all_info": "↪ Če se odločite, da ne želite posredovati vseh prejemnikov, boste morali za vsakega posameznega prejemnika, ki ga želite posredovati, dodati (\"slepi\") poštni predal.", + "relay_transport_info": "
    Informacije
    Za to domeno lahko določite transportne zemljevide za cilj po meri. Če niso nastavljeni, bo izvedeno iskanje MX.", + "save": "Shrani spremembe", + "scope": "Obseg", + "sender_acl": "Dovoli pošiljanje kot", + "sender_acl_disabled": "Preverjanje pošiljatelja je onemogočeno", + "sieve_type": "Vrsta filtra", + "skipcrossduplicates": "Preskoči podvojena sporočila med mapami (po principu \"kdor prej pride, prej melje\")", + "sogo_access": "Neposredno posredovanje na SOGo", + "sogo_access_info": "Po prijavi je uporabnik samodejno preusmerjen na SOGo.", + "sogo_visible": "Vzdevek je viden v SOGo", + "sogo_visible_info": "Ta možnost vpliva samo na objekte, ki jih je mogoče prikazati v SOGo (naslovi vzdevkov v skupni rabi ali brez nje, ki kažejo na vsaj en lokalni poštni predal). Če je skrita, vzdevek ne bo prikazan kot izbirni pošiljatelj v SOGo.", + "spam_alias": "Ustvarjanje ali spreminjanje časovno omejenih vzdevkovnih naslovov", + "spam_filter": "Filter neželene pošte", + "spam_policy": "Dodajanje ali odstranjevanje elementov na seznam dovoljenih/zavrnjenih", + "spam_score": "Nastavite oceno neželene pošte po meri", + "subfolder2": "Sinhroniziraj v podmapo na cilju
    (prazno = ne uporabi podmape)", + "syncjob": "Urejanje sinhronizacijskega opravila", + "target_address": "Pojdi na naslov/e (ločeno z vejico)", + "target_domain": "Ciljna domena", + "timeout1": "Časovna omejitev za povezavo z oddaljenim gostiteljem", + "timeout2": "Časovna omejitev za povezavo z lokalnim gostiteljem", + "title": "Urejanje predmeta", + "unchanged_if_empty": "Če ni spremenjeno, pustite prazno", + "username": "Uporabniško ime", + "validate_save": "Potrdi in shrani", + "mailbox_rename": "Preimenuj poštni predal", + "mailbox_rename_agree": "Ustvaril/a sem varnostno kopijo.", + "mailbox_rename_warning": "POMEMBNO! Pred preimenovanjem nabiralnika ustvarite varnostno kopijo.", + "sieve_desc": "Kratek opis", + "mta_sts": "MTA-STS", + "mta_sts_info": "MTA-STS je standard, ki vsiljuje dostavo e-pošte med poštnimi strežniki z uporabo TLS z veljavnimi potrdili.
    Uporablja se, kadar DANE ni mogoč zaradi manjkajočega ali nepodprtega DNSSEC.
    Opomba: Če prejemna domena podpira DANE z DNSSEC, je DANE vedno prednost – MTA-STS deluje le kot rezervna možnost.", + "mta_sts_version": "Različica", + "mta_sts_version_info": "Določa različico standarda MTA-STS – trenutno je veljavna samo različica STSv1.", + "mta_sts_mode": "Način", + "mta_sts_mode_info": "Na voljo so trije načini:
    • testiranje – pravilnik se samo spremlja, kršitve nimajo vpliva.
    • uveljavljanje – pravilnik se strogo uveljavlja, povezave brez veljavnega TLS so zavrnjene.
    • brez – pravilnik je objavljen, vendar se ne uporablja.
    ", + "mta_sts_max_age": "Najvišja starost", + "mta_sts_max_age_info": "Čas v sekundah, ki ga lahko prejemni poštni strežniki shranijo v predpomnilnik, dokler se ne naloži ponovno.", + "mta_sts_mx": "MX strežnik", + "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." + }, + "footer": { + "restart_container_info": "Pomembno: Eleganten ponovni zagon lahko traja nekaj časa, zato počakajte, da se konča.", + "delete_these_items": "Prosimo, potrdite spremembe naslednjega ID-ja objekta", + "confirm_delete": "Potrdi brisanje", + "delete_now": "Izbriši zdaj", + "hibp_check": "Preverite na haveibeenpwned.com", + "hibp_nok": "Ujema se! To je potencialno nevarno geslo!", + "hibp_ok": "Ni najdenega ujemanja.", + "loading": "Prosim, počakajte...", + "nothing_selected": "Nič izbranega", + "restart_container": "Znova zaženite zabojnik", + "restart_now": "Znova zaženi zdaj", + "restarting_container": "Ponovni zagon zabojnika, to lahko traja nekaj časa", + "cancel": "Prekliči" + }, + "login": { + "password": "Geslo", + "invalid_pass_reset_token": "Žeton za ponastavitev gesla je neveljaven ali je potekel.
    Zahtevajte novo povezavo za ponastavitev gesla.", + "back_to_mailcow": "Nazaj v mailcow", + "delayed": "Prijava je bila zakasnjena za %s sekund.", + "fido2_webauthn": "Prijava FIDO2/WebAuthent", + "forgot_password": "> Ste pozabili geslo?", + "login": "Prijava", + "login_linkstext": "Ni pravilna prijava?", + "login_usertext": "Prijava kot uporabnik", + "login_domainadmintext": "Prijava kot skrbnik domene", + "login_admintext": "Prijava kot skrbnik", + "login_user": "Prijava uporabnika", + "login_dadmin": "Prijava skrbnika domene", + "login_admin": "Prijava skrbnika", + "mobileconfig_info": "Za prenos zahtevanega profila povezave Apple se prijavite kot uporabnik poštnega predala.", + "new_password": "Novo geslo", + "new_password_confirm": "Potrdi novo geslo", + "other_logins": "ali se prijavite s/z", + "reset_password": "Ponastavi geslo", + "request_reset_password": "Zahteva za spremembo gesla", + "username": "Uporabniško ime", + "email": "E-poštni naslov" + }, + "mailbox": { + "last_mail_login": "Zadnja prijava v e-pošto", + "deactivate": "Deaktiviraj", + "domain_admins": "Skrbniki domen", + "kind": "Prijazno", + "mailbox_defaults_info": "Določite privzete nastavitve za nove poštne nabiralnike.", + "allowed_protocols": "Dovoljeni protokoli", + "add_alias": "Dodaj vzdevek", + "mins_interval": "Interval (min)", + "recipient_map_info": "Zemljevidi prejemnikov se uporabljajo za zamenjavo naslova prejemnika v sporočilu, preden je dostavljeno.", + "add_domain_record_first": "Najprej dodajte domeno", + "booking_ltnull": "Neomejeno, vendar se ob rezervaciji prikaži kot zasedeno", + "alias_domain_alias_hint": "Vzdevki se na vzdevke domen samodejno ne uporabijo. Naslov vzdevka my-alias@domain ne pokriva naslova my-alias@alias-domain (kjer je \"vzdevek domene\" namišljeni vzdevek domene za \"domeno\").
    Za preusmeritev pošte v zunanji nabiralnik uporabite filter sito (glejte zavihek »Filtri« ali uporabite SOGo -> Posrednik). Za samodejno dodajanje manjkajočih vzdevkov uporabite \"Razširi vzdevek čez domene vzdevkov\".", + "q_all": " ob premiku v mapo Neželena pošta in ob zavrnitvi", + "bcc_info": "BCC zemljevidi se uporabljajo za tiho posredovanje kopij vseh sporočil na drug naslov. Vnos vrste preslikave prejemnika se uporablja, kadar lokalni cilj deluje kot prejemnik pošte. Preslikave pošiljatelja delujejo po istem načelu.
    \n Lokalni cilj ne bo obveščen o neuspeli dostavi.", + "force_pw_update": "Vsiljena posodobitev gesla ob naslednji prijavi", + "recipient_map_new_info": "Cilj zemljevida prejemnika mora biti veljaven e-poštni naslov ali ime domene.", + "recipient_map_old_info": "Izvirni cilj prejemnika mora biti veljaven e-poštni naslov ali ime domene.", + "action": "Dejanje", + "activate": "Aktiviraj", + "active": "Aktivno", + "add": "Dodaj", + "add_alias_expand": "Razširi vzdevek nad vzdevki domen", + "add_bcc_entry": "Dodaj zemljevid BCC", + "add_domain": "Dodaj domeno", + "add_domain_alias": "Dodaj vzdevek domene", + "add_filter": "Dodaj filter", + "add_mailbox": "Dodaj poštni predal", + "add_recipient_map_entry": "Dodaj zemljevid prejemnikov", + "add_resource": "Dodaj vir", + "add_template": "Dodaj predlogo", + "add_tls_policy_map": "Dodaj zemljevid pravilnikov TLS", + "address_rewriting": "Prepisovanje naslovov", + "alias": "Vzdevek", + "alias_domain_backupmx": "Vzdevek domene ni aktiven za posredovalno domeno", + "aliases": "Vzdevki", + "all_domains": "Vse domene", + "allow_from_smtp": "Dovoli samo tem IP-jem uporabo SMTP", + "allow_from_smtp_info": "Pustite prazno, da dovolite vse pošiljatelje.
    Naslovi in omrežja IPv4/IPv6.", + "backup_mx": "Posredovalna domena", + "bcc": "BCC", + "bcc_destination": "Ciljna stran BCC", + "bcc_destinations": "Ciljna stran BCC", + "bcc_local_dest": "Lokalni cilj", + "bcc_map": "BCC zemljevid", + "bcc_map_type": "BCC tip", + "bcc_maps": "BCC zemljevidi", + "bcc_rcpt_map": "Zemljevid prejemnikov", + "bcc_sender_map": "Zemljevid pošiljatelja", + "bcc_to_rcpt": "Preklopi na vrsto zemljevida prejemnika", + "bcc_to_sender": "Preklopi na vrsto zemljevida pošiljatelja", + "bcc_type": "BCC tip", + "booking_null": "Vedno prikaži kot brezplačno", + "booking_0_short": "Vedno prost", + "booking_custom": "Točna omejitev na število rezervacij po meri", + "booking_custom_short": "Trda omejitev", + "booking_lt0_short": "Mehka omejitev", + "catch_all": "Zajemi vse", + "created_on": "Ustvarjeno dne", + "daily": "Dnevno", + "description": "Opis", + "disable_login": "Prepreči prijavo (dohodna pošta je še vedno sprejeta)", + "disable_x": "Onemogoči", + "dkim_domains_selector": "Izbirnik", + "dkim_key_length": "Dolžina ključa DKIM (biti)", + "domain": "Domena", + "domain_aliases": "Vzdevki domen", + "domain_templates": "Predloge domen", + "domain_quota": "Kvota", + "domain_quota_total": "Celotna kvota domene", + "domains": "Domene", + "edit": "Uredi", + "empty": "Ni rezultatov", + "enable_x": "Omogoči", + "filters": "Filtri", + "fname": "Polno ime", + "gal": "Globalni seznam naslovov", + "goto_ham": "Uči se kot ham", + "goto_spam": "Uči se kot spam", + "hourly": "Urno", + "iam": "Ponudnik identitete", + "in_use": "V uporabi (%)", + "inactive": "Neaktivno", + "insert_preset": "Vstavi primer prednastavitve \"%s\"", + "last_modified": "Zadnja sprememba", + "last_pw_change": "Zadnja sprememba gesla", + "last_run_reset": "Načrtuj naslednje", + "mailbox": "Poštni nabiralnik", + "mailbox_defaults": "Privzete nastavitve", + "mailbox_defquota": "Privzeta velikost poštnega nabiralnika", + "mailbox_templates": "Predloge poštnih nabiralnikov", + "mailbox_quota": "Največja velikost poštnega nabiralnika", + "mailboxes": "Poštni nabiralniki", + "max_aliases": "Največje število vzdevkov", + "max_mailboxes": "Največje možno število poštnih predalov", + "max_quota": "Največja kvota na poštni predal", + "msg_num": "Sporočilo #", + "multiple_bookings": "Več rezervacij", + "never": "Nikoli", + "no": "✕", + "no_record": "Ni zapisa za objekt %s", + "no_record_single": "Ni zapisa", + "open_logs": "Odpri dnevnike", + "owner": "Lastnik", + "private_comment": "Zasebni komentar", + "public_comment": "Javni komentar", + "q_add_header": "ko je premaknjeno v mapo Neželena pošta", + "q_reject": "ob zavrnitvi", + "quarantine_category": "Kategorija obvestil o karanteni", + "quarantine_notification": "Obvestila o karanteni", + "quick_actions": "Dejanja", + "recipient": "Prejemnik", + "recipient_map": "Zemljevid prejemnikov", + "recipient_map_new": "Nov prejemnik", + "recipient_map_old": "Prvotni prejemnik", + "recipient_maps": "Zemljevidi prejemnikov", + "relay_all": "Posreduj vsem prejemnikom", + "relay_unknown": "Posredovanje neznanih poštnih predalov", + "remove": "Odstrani", + "resources": "Viri", + "running": "V teku", + "sender": "Pošiljatelj", + "set_postfilter": "Označi kot postfilter", + "set_prefilter": "Označi kot predfilter", + "sieve_preset_1": "Zavrzi pošto z verjetno nevarnimi vrstami datotek", + "filter_table": "Filtriraj tabelo", + "excludes": "Izključuje", + "last_run": "Zadnji zagon", + "sieve_preset_2": "Vedno označi e-pošto določenega pošiljatelja kot videno", + "target_address": "Pojdi na naslov", + "syncjob_EXIT_TLS_FAILURE": "Težava s šifrirano povezavo", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Ni mogoče vzpostaviti povezave z oddaljenim strežnikom", + "tls_map_parameters_info": "Prazno ali parametri, na primer: protocols=!SSLv2 ciphers=medium exclude=3DES", + "tls_policy_maps_enforced_tls": "Ti pravilniki bodo preglasili tudi vedenje uporabnikov poštnih predalov, ki vsiljujejo odhodne povezave TLS. Če spodaj ni pravilnika, bodo ti uporabniki uporabili privzete vrednosti, določene kot smtp_tls_mandatory_protocols in smtp_tls_mandatory_ciphers.", + "spam_aliases": "Začasni vzdevek", + "sieve_preset_3": "Tiho zavrzite, ustavite vso nadaljnjo obdelavo s presejanjem", + "sieve_preset_4": "Vloži v mapo PREJETO, preskoči nadaljnjo obdelavo s sitastimi filtri", + "sieve_preset_5": "Samodejni odzivnik (dopust)", + "sieve_preset_6": "Zavrni pošto z odgovorom", + "sieve_preset_7": "Preusmeritev in ohrani/opusti", + "sieve_preset_8": "Preusmeritev e-pošte od določenega pošiljatelja, označitev kot prebrano in razvrščanje v podmapo", + "sieve_preset_header": "Spodaj si oglejte primere prednastavitev. Za več podrobnosti glejte Wikipedijo.", + "sogo_visible": "Vzdevek je viden v SOGo", + "sogo_visible_n": "Skrij vzdevek v SOGo", + "sogo_visible_y": "Prikaži vzdevek v SOGo", + "stats": "Statistika", + "status": "Stanje", + "sync_jobs": "Sinhronizacija opravil", + "syncjob_check_log": "Preveri dnevnik", + "syncjob_last_run_result": "Rezultat zadnjega zagona", + "syncjob_EX_OK": "Uspešno", + "syncjob_EXIT_CONNECTION_FAILURE": "Težava s povezavo", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Težava z overjanjem", + "syncjob_EXIT_OVERQUOTA": "Ciljni poštni predal presega kvoto", + "table_size_show_n": "Prikaži %s elementov", + "target_domain": "Ciljna domena", + "template": "Predloga", + "tls_enforce_in": "Uveljavi dohodni TLS", + "tls_enforce_out": "Uveljavi odhodni TLS", + "tls_map_dest": "Cilj", + "tls_map_dest_info": "Primeri: example.org, .example.org, [mail.example.org]:25", + "tls_map_parameters": "Parametri", + "tls_map_policy": "Politika", + "tls_policy_maps": "Zemljevidi pravilnikov TLS", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Napačno uporabniško ime ali geslo", + "templates": "Predloge", + "tls_policy_maps_long": "Preglasitve zemljevidov pravilnikov odhodnega TLS", + "table_size": "Velikost tabele", + "toggle_all": "Preklopi vse", + "username": "Uporabniško ime", + "waiting": "Čakanje", + "yes": "✓", + "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" + }, + "fido2": { + "known_ids": "Znani ID-ji", + "set_fido2_touchid": "Registrirajte Touch ID na Apple M1", + "confirm": "Potrdi", + "fido2_auth": "Prijava s FIDO2", + "fido2_success": "Naprava je bila uspešno registrirana", + "fido2_validation_failed": "Preverjanje ni uspelo", + "fn": "Prijazno ime", + "none": "Onemogočeno", + "register_status": "Status registracije", + "rename": "Preimenuj", + "set_fido2": "Registrirajte napravo FIDO2", + "set_fn": "Nastavi prijazno ime", + "start_fido2_validation": "Začni validacijo FIDO2" + }, + "info": { + "session_expires": "Vaša seja bo potekla čez približno 15 sekund", + "awaiting_tfa_confirmation": "Čakam na potrditev TFA", + "no_action": "Ni veljavnih ukrepov" + }, + "header": { + "administration": "Konfiguracija in podrobnosti", + "apps": "Aplikacije", + "debug": "Informacije", + "email": "E-pošta", + "mailcow_system": "Sistem", + "mailcow_config": "Konfiguracija", + "quarantine": "Karantena", + "restart_netfilter": "Znova zaženite omrežni filter", + "restart_sogo": "Znova zaženite SOGo", + "user_settings": "Uporabniške nastavitve" + }, + "quarantine": { + "learn_spam_delete": "Uči se kot neželena pošta in izbriši", + "medium_danger": "Srednje", + "notified": "Obveščen", + "low_danger": "Nizko", + "qinfo": "Sistem karantene bo zavrnjeno pošto shranil v zbirko podatkov (pošiljatelj ne bo imel vtisa, da je bila pošta dostavljena), prav tako pa bo pošto, ki bo dostavljena kot kopija, shranil v mapo »Neželena pošta« v nabiralniku.\n
    »Uči kot neželeno pošto in izbriši« bo sporočilo prepoznal kot neželeno pošto prek Bayesovega izreka in izračunal tudi mehke zgoščene vrednosti, da bi v prihodnje zavrnil podobna sporočila.\n
    Upoštevajte, da je učenje več sporočil lahko – odvisno od vašega sistema – zamudno.
    Elementi na seznamu zavrnjenih so izključeni iz karantene.", + "junk_folder": "Mapa z neželeno pošto", + "action": "Dejanje", + "atts": "Priloge", + "check_hash": "Iskanje zgoščene vrednosti datoteke @ VT", + "confirm": "Potrdi", + "confirm_delete": "Potrdite brisanje tega elementa.", + "danger": "Nevarnost", + "deliver_inbox": "Dostavi v mapo »Prejeto«", + "disabled_by_config": "Trenutna konfiguracija sistema onemogoča funkcionalnost karantene. Nastavite »število hranjenj na poštni predal« in »največjo velikost« za elemente karantene.", + "download_eml": "Prenesi (.eml)", + "empty": "Ni rezultatov", + "high_danger": "Visoko", + "info": "Informacija", + "neutral_danger": "Nevtralno", + "qhandler_success": "Zahteva je bila uspešno poslana v sistem. Zdaj lahko zaprete okno.", + "qid": "Rspamd QID", + "qitem": "Predmet iz karantene", + "spam_score": "Rezultat", + "text_from_html_content": "Vsebina (pretvorjen html)", + "quarantine": "Karantena", + "quick_delete_link": "Odpri povezavo za hitro brisanje", + "quick_info_link": "Odpri povezavo z informacijami", + "quick_release_link": "Odpri povezavo za hitro objavo", + "rcpt": "Prejemnik", + "received": "Prejeto", + "recipients": "Prejemniki", + "refresh": "Osveži", + "rejected": "Zavrnjeno", + "release": "Izdaja", + "release_body": "Vaše sporočilo smo priložili kot datoteko eml temu sporočilu.", + "release_subject": "Potencialno škodljiv predmet v karanteni %s", + "remove": "Odstrani", + "rewrite_subject": "Prepiši zadevo", + "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", + "show_item": "Prikaži element", + "spam": "Neželena pošta", + "subj": "Zadeva", + "table_size": "Velikost tabele", + "table_size_show_n": "Prikaži %s elementov", + "text_plain_content": "Vsebina (besedilo/navadno)", + "toggle_all": "Preklopi vse", + "type": "Vrsta", + "quick_actions": "Dejanja" + }, + "oauth2": { + "access_denied": "Za odobritev dostopa prek OAuth2 se prijavite kot lastnik poštnega predala.", + "authorize_app": "Avtorizacija aplikacije", + "deny": "Zavrni", + "permit": "Avtorizacija aplikacije", + "profile": "Profil", + "profile_desc": "Ogled osebnih podatkov: uporabniško ime, polno ime, ustvarjeno, spremenjeno, aktivno", + "scope_ask_permission": "Aplikacija je zahtevala naslednja dovoljenja" + }, + "queue": { + "info": "Čakalna vrsta pošte vsebuje vsa e-poštna sporočila, ki čakajo na dostavo. Če je e-poštno sporočilo dlje časa obtičalo v čakalni vrsti, ga sistem samodejno izbriše.
    Sporočilo o napaki ustreznega e-poštnega sporočila vsebuje informacije o tem, zakaj sporočila ni bilo mogoče dostaviti.", + "delete": "Izbriši vse", + "flush": "Izprazni čakalno vrsto", + "legend": "Funkcije dejanj čakalne vrste pošte:", + "ays": "Potrdite, da želite izbrisati vse elemente iz trenutne čakalne vrste.", + "deliver_mail": "Dostavi", + "deliver_mail_legend": "Poskusi ponovne dostave izbranih e-poštnih sporočil.", + "hold_mail": "Zadrži", + "hold_mail_legend": "Zadrži izbrano pošto. (Prepreči nadaljnje poskuse dostave)", + "queue_manager": "Upravitelj čakalnih vrst", + "show_message": "Prikaži sporočilo", + "unban": "odblokiraj čakalno vrsto", + "unhold_mail": "Nezadrži", + "unhold_mail_legend": "Sprosti izbrana sporočila za dostavo. (Zahteva predhodno zadržanje)" + }, + "success": { + "eas_reset": "Naprave ActiveSync za uporabnika %s so bile ponastavljene", + "alias_added": "Dodan je bil vzdevek %s (%d)", + "app_passwd_added": "Dodano novo geslo za aplikacijo", + "domain_add_dkim_available": "Ključ DKIM je že obstajal", + "custom_login_modified": "Prilagoditev prijave je bila uspešno shranjena", + "rl_saved": "Omejitev hitrosti za objekt %s je shranjena", + "domain_admin_modified": "Spremembe skrbnika domene %s so bile shranjene", + "license_modified": "Spremembe licence so bile shranjene", + "pushover_settings_edited": "Nastavitve Pushoverja so bile uspešno nastavljene, preverite poverilnice.", + "mailbox_renamed": "Poštni nabiralnik je bil preimenovan iz %s v %s", + "recovery_email_sent": "E-poštno sporočilo za obnovitev je bilo poslano na %s", + "relayhost_removed": "Vnos na zemljevidu %s je bil odstranjen", + "acl_saved": "ACL za objekt %s shranjen", + "admin_added": "Dodan je bil skrbnik %s", + "admin_api_modified": "Spremembe API-ja so bile shranjene", + "admin_modified": "Spremembe skrbnika so bile shranjene", + "admin_removed": "Administrator %s je bil odstranjen", + "alias_domain_removed": "Vzdevek domene %s je bil odstranjen", + "alias_modified": "Spremembe vzdevka %s so bile shranjene", + "alias_removed": "Vzdevek %s je bil odstranjen", + "aliasd_added": "Dodan vzdevek domene %s", + "aliasd_modified": "Spremembe vzdevka domene %s so bile shranjene", + "app_links": "Shranjene spremembe povezav aplikacij", + "app_passwd_removed": "Odstranjen ID gesla za aplikacijo %s", + "bcc_deleted": "Vnosi na zemljevidu BCC so izbrisani: %s", + "bcc_edited": "Vnos na zemljevidu BCC %s je bil urejen", + "db_init_complete": "Inicializacija baze podatkov končana", + "delete_filter": "Izbrisani filtri ID %s", + "delete_filters": "Izbrisani filtri: %s", + "deleted_syncjob": "Izbrisan ID sinhronizacijskega opravila %s", + "deleted_syncjobs": "Izbrisana opravila sinhronizacije: %s", + "dkim_added": "Ključ DKIM %s je bil shranjen", + "dkim_duplicated": "Ključ DKIM za domeno %s je bil kopiran v %s", + "dkim_removed": "Ključ DKIM %s je bil odstranjen", + "domain_added": "Dodana domena %s", + "domain_admin_added": "Dodan je bil skrbnik domene %s", + "domain_admin_removed": "Skrbnik domene %s je bil odstranjen", + "domain_footer_modified": "Spremembe v nogi domene %s so bile shranjene", + "domain_modified": "Spremembe domene %s so bile shranjene", + "domain_removed": "Domena %s je bila odstranjena", + "dovecot_restart_success": "Dovecot je bil uspešno ponovno zagnan", + "f2b_banlist_refreshed": "ID seznama prepovedanih je bil uspešno osvežen.", + "f2b_modified": "Spremembe parametrov Fail2ban so bile shranjene", + "forwarding_host_added": "Dodan je bil posredovalni gostitelj %s", + "forwarding_host_removed": "Gostitelj za posredovanje %s je bil odstranjen", + "global_filter_written": "Filter je bil uspešno zapisan v datoteko", + "hash_deleted": "Zgoščena vrednost je izbrisana", + "iam_test_connection": "Povezava je bila uspešna", + "ip_check_opt_in_modified": "Preverjanje IP-ja je bilo uspešno shranjeno", + "item_deleted": "Element %s je bil uspešno izbrisan", + "items_deleted": "Element %s je bil uspešno izbrisan", + "items_released": "Izbrani elementi so bili izdani", + "logged_in_as": "Prijavljen kot %s", + "mailbox_added": "Dodan je bil poštni predal %s", + "mailbox_modified": "Spremembe poštnega nabiralnika %s so bile shranjene", + "mailbox_removed": "Poštni nabiralnik %s je bil odstranjen", + "nginx_reloaded": "Nginx je bil ponovno naložen", + "object_modified": "Spremembe objekta %s so bile shranjene", + "password_policy_saved": "Pravilnik o geslih je bil uspešno shranjen", + "password_changed_success": "Geslo je bilo uspešno spremenjeno", + "qlearn_spam": "Sporočilo z ID-jem %s je bilo prepoznano kot neželena pošta in izbrisano", + "queue_command_success": "Ukaz za čakalno vrsto je bil uspešno zaključen", + "recipient_map_entry_deleted": "ID zemljevida prejemnika %s je bil izbrisan", + "recipient_map_entry_saved": "Vnos zemljevida prejemnika »%s« je bil shranjen", + "relayhost_added": "Dodan je bil vnos na zemljevid %s", + "reset_main_logo": "Ponastavi na privzeti logotip", + "resource_added": "Dodan je bil vir %s", + "resource_modified": "Spremembe poštnega nabiralnika %s so bile shranjene", + "resource_removed": "Vir %s je bil odstranjen", + "rspamd_ui_pw_set": "Geslo uporabniškega vmesnika Rspamd je bilo uspešno nastavljeno", + "settings_map_added": "Dodan vnos nastavitev zemljevida", + "settings_map_removed": "Odstranjen ID zemljevida nastavitev %s", + "sogo_profile_reset": "Profil SOGo za uporabnika %s je bil ponastavljen", + "template_added": "Dodana predloga %s", + "template_modified": "Spremembe predloge %s so bile shranjene", + "template_removed": "ID predloge %s je bil izbrisan", + "tls_policy_map_entry_deleted": "ID zemljevida pravilnika TLS %s je bil izbrisan", + "tls_policy_map_entry_saved": "Vnos zemljevida pravilnika TLS »%s« je bil shranjen", + "ui_texts": "Shranjene spremembe besedil uporabniškega vmesnika", + "upload_success": "Datoteka je bila uspešno naložena", + "verified_fido2_login": "Preverjena prijava v FIDO2", + "verified_totp_login": "Preverjena prijava v TOTP", + "verified_webauthn_login": "Preverjena prijava v WebAuth", + "verified_yotp_login": "Preverjena prijava z enkratnim geslom Yubico", + "bcc_saved": "Vnos na zemljevidu BCC je shranjen", + "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" + }, + "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.", + "help": "Prikaži/skrij ploščo s pomočjo" + }, + "tfa": { + "api_register": "%s uporablja Yubico Cloud API. Pridobite API ključ za svoj ključ tukaj", + "authenticators": "Preverjevalniki pristnosti", + "confirm": "Potrdi", + "delete_tfa": "Onemogoči TFA", + "disable_tfa": "Onemogoči TFA do naslednje uspešne prijave", + "enter_qr_code": "Vaša koda TOTP, če vaša naprava ne more skenirati kod QR", + "error_code": "Koda napake", + "confirm_totp_token": "Spremembe potrdite z vnosom ustvarjenega žetona", + "tfa": "Dvofaktorska avtentikacija", + "webauthn": "Preverjanje pristnosti WebAuthn", + "none": "Deaktiviraj", + "init_webauthn": "Inicializacija, prosim počakajte...", + "key_id": "Identifikator za vašo napravo", + "key_id_totp": "Identifikator za vaš ključ", + "reload_retry": "- (če napaka vztraja, znova zaženite brskalnik)", + "scan_qr_code": "Prosimo, skenirajte naslednjo kodo z aplikacijo za preverjanje pristnosti ali jo vnesite ročno.", + "select": "Prosimo, izberite", + "set_tfa": "Nastavite metodo dvofaktorske avtentikacije", + "start_webauthn_validation": "Začni validacijo", + "tfa_token_invalid": "Žeton TFA neveljaven", + "totp": "Enkratno geslo na podlagi časa (Google Authenticator, Authy itd.)", + "u2f_deprecated": "Zdi se, da je bil vaš ključ registriran z zastarelo metodo U2F. Deaktivirali bomo dvofaktorsko overjanje in izbrisali vaš ključ.", + "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" + }, + "ratelimit": { + "disabled": "Onemogočeno", + "second": "sporočil / sekundo", + "minute": "sporočil / minuto", + "hour": "sporočil / uro", + "day": "sporočil / dan" + }, + "user": { + "create_syncjob": "Ustvari novo sinhronizacijsko opravilo", + "open_webmail_sso": "Spletna pošta", + "attribute": "Atribut", + "description": "Opis", + "direct_aliases_desc": "Na neposredne vzdevke vplivajo nastavitve filtra neželene pošte in pravilnika TLS.", + "direct_protocol_access": "Ta uporabnik poštnega predala ima neposreden zunanji dostop do naslednjih protokolov in aplikacij. To nastavitev nadzoruje vaš skrbnik. Za dostop do posameznih protokolov in aplikacij je mogoče ustvariti gesla za aplikacije.
    Gumb »Spletna pošta« omogoča enotno prijavo v SOGo in je vedno na voljo.", + "shared_aliases_desc": "Na uporabniške nastavitve, kot sta filter neželene pošte ali pravilnik za šifriranje, ne vplivajo skupni vzdevki. Ustrezne filtre neželene pošte lahko nastavi le skrbnik kot pravilnik za celotno domeno.", + "force_pw_update": "Za dostop do storitev, povezanih s skupinsko programsko opremo, morate nastaviti novo geslo.", + "new_password": "Novo geslo", + "password_reset_info": "Če ni naveden e-poštni naslov za obnovitev gesla, te funkcije ni mogoče uporabiti.", + "pushover_sender_array": "Upoštevajte naslednje e-poštne naslove pošiljateljev (ločeni z vejico)", + "tag_help_explain": "V podmapi: pod mapo INBOX (\"INBOX/Facebook\") bo ustvarjena nova podmapa, poimenovana po oznaki.
    \nV zadevi: ime oznake bo dodano pred zadevo e-poštnega sporočila, na primer: \"[Facebook] Moje novice\".", + "pushover_vars": "Če filter pošiljatelja ni definiran, bodo upoštevana vsa e-poštna sporočila.
    Filtre regularnih izrazov in natančna preverjanja pošiljateljev je mogoče definirati posamično in bodo obravnavana zaporedno. Niso odvisna drug od drugega.
    Uporabne spremenljivke za besedilo in naslov (upoštevajte pravilnike o varstvu podatkov)", + "quarantine_notification_info": "Ko je obvestilo poslano, bodo elementi označeni kot »obveščeni« in za ta določen element ne bodo poslana nobena nadaljnja obvestila.", + "verify": "Preveri", + "spamfilter_bl_desc": "E-poštni naslovi na seznamu zavrnjenih, ki bodo vedno razvrščeni kot neželena pošta in zavrnjeni. Zavrnjena pošta ne bo kopirana v karanteno. Uporabite lahko nadomestne znake. Filter se uporabi samo za neposredne vzdevke (vzdevke z enim samim ciljnim nabiralnikom), izključujoč vseobsegajoče vzdevke in sam nabiralnik.", + "spamfilter_wl_desc": "E-poštni naslovi na seznamu dovoljenih so programirani tako, da se nikoli ne razvrstijo kot neželena pošta. Uporabijo se lahko nadomestni znaki. Filter se uporabi samo za neposredne vzdevke (vzdevke z enim samim ciljnim poštnim predalom), izključujoč vseobsegajoče vzdevke in sam poštni predal.", + "tls_policy_warning": "Opozorilo: Če se odločite za uveljavitev šifriranega prenosa pošte, lahko izgubite e-pošto.
    Sporočila, ki ne ustrezajo pravilniku, bo poštni sistem zavrnil s popolno napako.
    Ta možnost velja za vaš primarni e-poštni naslov (prijavno ime), vse naslove, izpeljane iz vzdevkov domen, in vzdevke, ki imajo samo ta en poštni predal kot cilj.", + "allowed_protocols": "Dovoljeni protokoli", + "title": "Naslov", + "action": "Dejanje", + "active": "Aktivno", + "active_sieve": "Aktiven filter", + "advanced_settings": "Napredne nastavitve", + "alias": "Vzdevek", + "alias_create_random": "Generiraj naključne vzdevke", + "alias_extend_all": "Podaljšaj vzdevek za 1 uro", + "alias_full_date": "d.m.Y, H:i:s T", + "alias_remove_all": "Odstrani vse vzdevke", + "alias_select_validity": "Obdobje veljavnosti", + "alias_time_left": "Preostali čas", + "alias_valid_until": "Veljavno do", + "aliases_also_send_as": "Pošiljanje je dovoljeno tudi kot uporabnik", + "aliases_send_as_all": "Ne preverjaj dostopa pošiljatelja za naslednje domene in njihove vzdevke", + "app_hint": "Gesla aplikacij so alternativna gesla za prijavo v IMAP, SMTP, CalDAV, CardDAV in EAS. Uporabniško ime ostane nespremenjeno. Spletna pošta SOGo ni na voljo prek gesel aplikacij.", + "app_name": "Ime aplikacije", + "app_passwds": "Gesla aplikacij", + "apple_connection_profile": "Profil povezave Apple", + "apple_connection_profile_complete": "Ta profil povezave vključuje parametre IMAP in SMTP ter poti CalDAV (koledarji) in CardDAV (stiki) za napravo Apple.", + "apple_connection_profile_mailonly": "Ta profil povezave vključuje konfiguracijske parametre IMAP in SMTP za napravo Apple.", + "apple_connection_profile_with_app_password": "Novo geslo za aplikacijo se ustvari in doda v profil, tako da pri nastavitvi naprave ni treba vnesti gesla. Datoteke ne delite, saj omogoča poln dostop do vašega nabiralnika.", + "authentication": "Avtentikacija", + "change_password": "Spremeni geslo", + "change_password_hint_app_passwords": "Vaš račun ima %d gesel aplikacij, ki ne bodo spremenjena. Če jih želite upravljati, odprite zavihek Gesla aplikacij.", + "clear_recent_successful_connections": "Jasno vidne uspešne povezave", + "client_configuration": "Prikaži vodnike za konfiguracijo e-poštnih odjemalcev in pametnih telefonov", + "create_app_passwd": "Ustvari geslo za aplikacijo", + "created_on": "Ustvarjeno dne", + "daily": "Dnevno", + "day": "dan", + "delete_ays": "Prosimo, potrdite postopek brisanja.", + "direct_aliases": "Neposredni vzdevki", + "eas_reset": "Ponastavi predpomnilnik naprave ActiveSync", + "eas_reset_help": "V mnogih primerih bo ponastavitev predpomnilnika naprave pomagala obnoviti pokvarjen profil ActiveSync.
    Pozor: Vsi elementi bodo ponovno preneseni!", + "eas_reset_now": "Ponastavi zdaj", + "edit": "Uredi", + "email": "E-pošta", + "email_and_dav": "E-pošta, koledarji in stiki", + "empty": "Ni rezultatov", + "encryption": "Šifriranje", + "excludes": "Izključuje", + "expire_in": "Poteče čez", + "fido2_webauthn": "FIDO2/WebAuthn", + "from": "od", + "generate": "ustvari", + "hour": "ura", + "hourly": "Urno", + "hours": "ure", + "in_use": "Uporabljeno", + "interval": "Interval", + "is_catch_all": "Vseobsegajoča povezava za domeno/e", + "last_mail_login": "Zadnja prijava v e-pošto", + "last_pw_change": "Zadnja sprememba gesla", + "last_run": "Zadnji zagon", + "last_ui_login": "Zadnja prijava v uporabniški vmesnik", + "loading": "Nalaganje ...", + "login_history": "Zgodovina prijav", + "mailbox": "Poštni nabiralnik", + "mailbox_details": "Podrobnosti", + "mailbox_general": "Splošno", + "mailbox_settings": "Nastavitve", + "messages": "sporočila", + "month": "mesec", + "months": "meseci", + "never": "Nikoli", + "new_password_repeat": "Potrditveno geslo (ponovite)", + "no_active_filter": "Ni aktivnega filtra", + "no_last_login": "Ni zadnjih podatkov za prijavo v uporabniški vmesnik", + "no_record": "Ni zapisa", + "open_logs": "Odpri dnevnike", + "overview": "Pregled", + "password": "Geslo", + "password_now": "Trenutno geslo (potrdite spremembe)", + "password_repeat": "Geslo (ponovite)", + "protocols": "Protokoli", + "pushover_evaluate_x_prio": "Eskalacija e-pošte z visoko prioriteto [X-Priority: 1]", + "pushover_info": "Nastavitve potisnih obvestil bodo veljale za vso čisto (ne neželeno) pošto, dostavljeno na %s, vključno z vzdevki (v skupni rabi, brez skupne rabe, označeni).", + "pushover_only_x_prio": "Upoštevaj samo pošto z visoko prioriteto [X-Priority: 1]", + "pushover_sender_regex": "Poišči ujemanje pošiljateljev z naslednjim regularnim izrazom", + "pushover_text": "Besedilo obvestila", + "pushover_title": "Naslov obvestila", + "pushover_sound": "Zvok", + "pushover_verify": "Preverite poverilnice", + "pw_recovery_email": "E-poštno sporočilo za obnovitev gesla", + "q_add_header": "Mapa z neželeno pošto", + "q_reject": "Zavrnjeno", + "quarantine_category": "Kategorija obvestil o karanteni", + "quarantine_category_info": "Kategorija obvestil »Zavrnjeno« vključuje zavrnjeno pošto, medtem ko »Mapa z neželeno pošto« obvesti uporabnika o e-pošti, ki je bila premaknjena v mapo z neželeno pošto.", + "quarantine_notification": "Obvestila o karanteni", + "recent_successful_connections": "Videne uspešne povezave", + "remove": "Odstrani", + "running": "V teku", + "save": "Shrani spremembe", + "save_changes": "Shrani spremembe", + "sender_acl_disabled": "Preverjanje pošiljatelja je onemogočeno", + "shared_aliases": "Skupni vzdevki", + "show_sieve_filters": "Prikaži filter sita aktivnega uporabnika", + "sogo_profile_reset": "Ponastavi profil SOGo", + "sogo_profile_reset_help": "S tem boste uničili uporabnikov profil SOGo in nepovratno izbrisali vse stike in podatke koledarja.", + "sogo_profile_reset_now": "Ponastavi profil zdaj", + "spam_aliases": "Vzdevki neželene e-pošte", + "spam_score_reset": "Ponastavi na privzete nastavitve strežnika", + "spamfilter": "Filter neželene pošte", + "spamfilter_behavior": "Ocena", + "spamfilter_bl": "Seznam zavrnjenih", + "spamfilter_default_score": "Privzete vrednosti", + "spamfilter_green": "Zelena: to sporočilo ni neželena pošta", + "spamfilter_hint": "Prva vrednost opisuje »nizko oceno neželene pošte«, druga pa »visoko oceno neželene pošte«.", + "spamfilter_red": "Rdeča: To sporočilo je neželena pošta in ga bo strežnik zavrnil", + "spamfilter_table_action": "Dejanje", + "spamfilter_table_add": "Dodaj element", + "spamfilter_table_domain_policy": "ni na voljo (pravilnik domene)", + "spamfilter_table_empty": "Ni podatkov za prikaz", + "spamfilter_table_remove": "odstrani", + "spamfilter_table_rule": "Pravilo", + "spamfilter_wl": "Seznam dovoljenih", + "spamfilter_yellow": "Rumena: to sporočilo je morda neželena pošta, označeno bo kot neželena pošta in premaknjeno v mapo z neželeno pošto", + "status": "Stanje", + "sync_jobs": "Sinhronizacija opravil", + "syncjob_check_log": "Preveri dnevnik", + "syncjob_last_run_result": "Rezultat zadnjega zagona", + "syncjob_EXIT_CONNECTION_FAILURE": "Težava s povezavo", + "syncjob_EXIT_TLS_FAILURE": "Težava s šifrirano povezavo", + "syncjob_EXIT_AUTHENTICATION_FAILURE": "Težava z overjanjem", + "syncjob_EXIT_OVERQUOTA": "Ciljni poštni predal presega kvoto", + "syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Ni mogoče vzpostaviti povezave z oddaljenim strežnikom", + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Napačno uporabniško ime ali geslo", + "tag_handling": "Nastavitev obravnave označene pošte", + "tag_help_example": "Primer označenega e-poštnega naslova: jaz+Facebook@example.org", + "tag_in_none": "Ne naredi ničesar", + "tag_in_subfolder": "V podmapi", + "tag_in_subject": "V zadevi", + "text": "Besedilo", + "tfa_info": "Dvofaktorska avtentikacija pomaga zaščititi vaš račun. Če jo omogočite, boste za prijavo v aplikacije ali storitve, ki ne podpirajo dvofaktorske avtentikacije (npr. poštni odjemalci), potrebovali gesla za aplikacije.", + "tls_enforce_in": "Uveljavi dohodni TLS", + "tls_enforce_out": "Uveljavi odhodni TLS", + "tls_policy": "Pravilnik o šifriranju", + "user_settings": "Uporabniške nastavitve", + "username": "Uporabniško ime", + "value": "Vrednost", + "week": "teden", + "weekly": "Tedensko", + "weeks": "tedni", + "with_app_password": "z geslom za aplikacijo", + "year": "leto", + "years": "leta", + "waiting": "Čakanje", + "q_all": "Vse kategorije", + "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." + }, + "warning": { + "cannot_delete_self": "Prijavljenega uporabnika ni mogoče izbrisati", + "domain_added_sogo_failed": "Domena je bila dodana, vendar ponovni zagon SOGo ni uspel. Preverite dnevnike strežnika.", + "dovecot_restart_failed": "Dovecota ni uspelo znova zagnati, preverite dnevnike", + "fuzzy_learn_error": "Napaka učenja mehkega zgoščevanja: %s", + "hash_not_found": "Zgoščena vrednost ni bila najdena ali je bila že izbrisana", + "ip_invalid": "Preskočen neveljaven IP: %s", + "is_not_primary_alias": "Preskočen neprimarni vzdevek %s", + "no_active_admin": "Zadnjega aktivnega skrbnika ni mogoče deaktivirati", + "quota_exceeded_scope": "Kvota domene presežena: V tem obsegu domene je mogoče ustvariti le neomejeno število poštnih predalov.", + "session_token": "Neveljaven žeton obrazca: Neujemanje žetonov", + "session_ua": "Neveljaven žeton obrazca: Napaka pri preverjanju uporabniškega agenta" } } diff --git a/mailcow/data/web/lang/lang.tr-tr.json b/mailcow/data/web/lang/lang.tr-tr.json index aa1b90d..bc1da77 100644 --- a/mailcow/data/web/lang/lang.tr-tr.json +++ b/mailcow/data/web/lang/lang.tr-tr.json @@ -1309,4 +1309,4 @@ "q_reject": "Reddedildi", "week": "Hafta" } -} \ No newline at end of file +} diff --git a/mailcow/data/web/lang/lang.uk-ua.json b/mailcow/data/web/lang/lang.uk-ua.json index c6ded24..61c401a 100644 --- a/mailcow/data/web/lang/lang.uk-ua.json +++ b/mailcow/data/web/lang/lang.uk-ua.json @@ -1307,4 +1307,4 @@ }, "collapse_all": "Згорнути все" } -} \ No newline at end of file +} diff --git a/mailcow/data/web/lang/lang.vi-vn.json b/mailcow/data/web/lang/lang.vi-vn.json new file mode 100644 index 0000000..0e9e6a2 --- /dev/null +++ b/mailcow/data/web/lang/lang.vi-vn.json @@ -0,0 +1,697 @@ +{ + "acl": { + "alias_domains": "Thêm tên miền bí danh", + "app_passwds": "Quản lý mật khẩu ứng dụng", + "bcc_maps": "Ánh xạ BCC", + "delimiter_action": "Hành động phân cách", + "domain_desc": "Thay đổi mô tả tên miền", + "domain_relayhost": "Thay đổi máy chủ chuyển tiếp cho tên miền", + "eas_reset": "Đặt lại thiết bị EAS", + "extend_sender_acl": "Cho phép mở rộng ACL người gửi bằng địa chỉ bên ngoài", + "filters": "Bộ lọc", + "login_as": "Đăng nhập với tư cách người dùng hộp thư", + "mailbox_relayhost": "Thay đổi máy chủ chuyển tiếp cho hộp thư", + "prohibited": "Bị cấm bởi ACL", + "protocol_access": "Thay đổi quyền truy cập giao thức", + "pushover": "Thông báo đẩy", + "pw_reset": "Cho phép đặt lại mật khẩu người dùng mailcow", + "quarantine": "Hành động cách ly", + "quarantine_attachments": "Cách ly tệp đính kèm", + "quarantine_category": "Thay đổi danh mục thông báo cách ly", + "quarantine_notification": "Thay đổi thông báo cách ly", + "ratelimit": "Giới hạn tốc độ", + "recipient_maps": "Ánh xạ người nhận", + "smtp_ip_access": "Thay đổi máy chủ được phép cho SMTP", + "sogo_access": "Cho phép quản lý truy cập SOGo", + "sogo_profile_reset": "Đặt lại hồ sơ SOGo", + "spam_alias": "Bí danh tạm thời", + "spam_policy": "Danh sách chặn/Danh sách cho phép", + "spam_score": "Điểm thư rác", + "syncjobs": "Công việc đồng bộ", + "tls_policy": "Chính sách TLS", + "unlimited_quota": "Hạn ngạch không giới hạn cho hộp thư" + }, + "add": { + "activate_filter_warn": "Tất cả các bộ lọc khác sẽ bị vô hiệu hóa khi tùy chọn kích hoạt được chọn.", + "active": "Đang hoạt động", + "add": "Thêm", + "add_domain_only": "Chỉ thêm tên miền", + "add_domain_restart": "Thêm tên miền và khởi động lại SOGo", + "alias_address": "Địa chỉ bí danh", + "alias_address_info": "Địa chỉ email đầy đủ hoặc @example.com để bắt tất cả thư cho một tên miền (phân cách bằng dấu phẩy). Chỉ áp dụng cho tên miền mailcow.", + "alias_domain": "Tên miền bí danh", + "alias_domain_info": "Chỉ cho phép tên miền hợp lệ (phân cách bằng dấu phẩy).", + "app_name": "Tên ứng dụng", + "app_password": "Thêm mật khẩu ứng dụng", + "app_passwd_protocols": "Các giao thức được phép cho mật khẩu ứng dụng", + "automap": "Thử tự động ánh xạ thư mục (\"Mục đã gửi\", \"Đã gửi\" => \"Đã gửi\" v.v.)", + "backup_mx_options": "Tùy chọn chuyển tiếp", + "bcc_dest_format": "Địa chỉ BCC phải là một địa chỉ email hợp lệ duy nhất.
    Nếu bạn cần gửi bản sao đến nhiều địa chỉ, hãy tạo một bí danh và sử dụng nó ở đây.", + "comment_info": "Bình luận riêng tư không hiển thị với người dùng, trong khi bình luận công khai được hiển thị dưới dạng chú thích khi di chuột qua trong tổng quan người dùng", + "custom_params": "Tham số tùy chỉnh", + "custom_params_hint": "Đúng: --param=xy, sai: --param xy", + "delete1": "Xóa từ nguồn khi hoàn thành", + "delete2": "Xóa thư ở đích không có ở nguồn", + "delete2duplicates": "Xóa các bản sao ở đích", + "description": "Mô tả", + "destination": "Đích", + "disable_login": "Không cho phép đăng nhập (vẫn nhận được thư đến)", + "domain": "Tên miền", + "domain_matches_hostname": "Tên miền %s khớp với tên máy chủ", + "domain_quota_m": "Tổng hạn ngạch tên miền (MiB)", + "dry": "Mô phỏng đồng bộ hóa", + "enc_method": "Phương thức mã hóa", + "exclude": "Loại trừ đối tượng (biểu thức chính quy)", + "full_name": "Tên đầy đủ", + "gal": "Danh sách địa chỉ toàn cục", + "gal_info": "GAL chứa tất cả các đối tượng của một tên miền và không thể được chỉnh sửa bởi bất kỳ người dùng nào. Thông tin rảnh/bận trong SOGo sẽ bị thiếu nếu vô hiệu hóa! Khởi động lại SOGo để áp dụng thay đổi.", + "generate": "Tạo", + "goto_ham": "\"Học là thư bình thường", + "goto_null": "Loại bỏ thư một cách thầm lặng", + "goto_spam": "Học là thư rác", + "hostname": "Máy chủ", + "inactive": "Không hoạt động", + "internal": "Nội bộ", + "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", + "mailbox_quota_def": "Hạn ngạch hộp thư mặc định", + "mailbox_quota_m": "Hạn ngạch tối đa mỗi hộp thư (MiB)", + "mailbox_username": "Tên người dùng (phần bên trái của địa chỉ email)", + "max_aliases": "Số lượng bí danh tối đa có thể tạo", + "max_mailboxes": "Số lượng hộp thư tối đa có thể tạo", + "mins_interval": "Khoảng thời gian kiểm tra (phút)", + "multiple_bookings": "Đặt chỗ nhiều lần", + "nexthop": "Bước nhảy tiếp theo", + "password": "Mật khẩu", + "password_repeat": "Xác nhận mật khẩu (nhập lại)", + "port": "Cổng", + "post_domain_add": "Container SOGo, \\\"sogo-mailcow\\\", cần được khởi động lại sau khi thêm tên miền mới!

    Ngoài ra, cấu hình DNS của tên miền cần được xem xét. Khi cấu hình DNS được phê duyệt, khởi động lại \\\"acme-mailcow\\\" để tự động tạo chứng chỉ cho tên miền mới của bạn (autoconfig.<domain>, autodiscover.<domain>).
    Bước này là tùy chọn và sẽ được thử lại sau mỗi 24 giờ.", + "private_comment": "Bình luận riêng tư", + "public_comment": "Bình luận công khai", + "quota_mb": "Hạn ngạch (MiB)", + "relay_all": "Chuyển tiếp tất cả người nhận", + "relay_all_info": "↪ Nếu bạn chọn không chuyển tiếp tất cả người nhận, bạn sẽ cần thêm một hộp thư (\"ẩn\") cho từng người nhận cần được chuyển tiếp.", + "relay_domain": "Chuyển tiếp tên miền này", + "relay_transport_info": "
    Thông tin
    Bạn có thể định nghĩa ánh xạ vận chuyển cho đích tùy chỉnh cho tên miền này. Nếu không được đặt, tra cứu MX sẽ được thực hiện.", + "relay_unknown_only": "Chỉ chuyển tiếp hộp thư không tồn tại. Hộp thư hiện có sẽ được gửi cục bộ.", + "relayhost_wrapped_tls_info": "Vui lòng không sử dụng các cổng được bọc TLS (thường được sử dụng trên cổng 465).
    \nSử dụng bất kỳ cổng không được bọc nào và sử dụng STARTTLS. Chính sách TLS để thực thi TLS có thể được tạo trong \"Ánh xạ chính sách TLS\".", + "select": "Vui lòng chọn...", + "select_domain": "Vui lòng chọn tên miền trước", + "sieve_desc": "Mô tả ngắn", + "sieve_type": "Loại bộ lọc", + "skipcrossduplicates": "Bỏ qua tin nhắn trùng lặp giữa các thư mục (ai đến trước được phục vụ trước)", + "subscribeall": "Đăng ký tất cả thư mục", + "syncjob": "Thêm công việc đồng bộ", + "syncjob_hint": "Lưu ý rằng mật khẩu cần được lưu dưới dạng văn bản thuần!", + "tags": "Thẻ", + "target_address": "Địa chỉ chuyển tiếp", + "target_address_info": "Địa chỉ email đầy đủ (phân cách bằng dấu phẩy).", + "target_domain": "Tên miền đích", + "timeout1": "Thời gian chờ kết nối đến máy chủ từ xa", + "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" + }, + "admin": { + "access": "Truy cập", + "action": "Hành động", + "activate_api": "Kích hoạt API", + "activate_send": "Kích hoạt nút gửi", + "active": "Đang hoạt động", + "active_rspamd_settings_map": "Ánh xạ cài đặt đang hoạt động", + "add": "Thêm", + "add_admin": "Thêm quản trị viên", + "add_domain_admin": "Thêm quản trị viên tên miền", + "add_forwarding_host": "Thêm máy chủ chuyển tiếp", + "add_relayhost": "Thêm vận chuyển phụ thuộc người gửi", + "add_relayhost_hint": "Vui lòng lưu ý rằng dữ liệu xác thực, nếu có, sẽ được lưu trữ dưới dạng văn bản thuần.", + "add_row": "Thêm hàng", + "add_settings_rule": "Thêm quy tắc cài đặt", + "add_transport": "Thêm vận chuyển", + "add_transports_hint": "Vui lòng lưu ý rằng dữ liệu xác thực, nếu có, sẽ được lưu trữ dưới dạng văn bản thuần.", + "additional_rows": " hàng bổ sung đã được thêm", + "admin": "Quản trị viên", + "admin_details": "Chỉnh sửa chi tiết quản trị viên", + "admin_domains": "Gán tên miền", + "admins": "Những quản trị viên", + "admins_ldap": "Quản trị viên LDAP", + "admin_quicklink": "Ẩn liên kết nhanh đến trang đăng nhập quản trị", + "advanced_settings": "Cài đặt nâng cao", + "allowed_methods": "Phương thức cho phép kiểm soát truy cập", + "allowed_origins": "Nguồn gốc cho phép kiểm soát truy cập", + "api_allow_from": "Cho phép truy cập API từ các IP/ký hiệu mạng CIDR này", + "api_info": "API đang được phát triển. Tài liệu có thể được tìm thấy tại /api", + "api_key": "Khóa API", + "api_read_only": "Truy cập chỉ đọc", + "api_read_write": "Truy cập đọc-ghi", + "api_skip_ip_check": "Bỏ qua kiểm tra IP cho API", + "app_hide": "Ẩn khi đăng nhập", + "app_links": "Liên kết ứng dụng", + "app_name": "Tên ứng dụng", + "apps_name": "Tên \"Ứng dụng mailcow\"", + "arrival_time": "Thời gian đến (giờ máy chủ)", + "authed_user": "Người dùng đã xác thực", + "ays": "Bạn có chắc chắn muốn tiếp tục không?", + "ban_list_info": "Xem danh sách IP bị cấm bên dưới: mạng (thời gian cấm còn lại) - [hành động].
    IP trong hàng đợi bỏ cấm sẽ được xóa khỏi danh sách cấm hoạt động trong vài giây.
    Nhãn đỏ cho biết lệnh cấm vĩnh viễn đang hoạt động bởi danh sách từ chối.", + "change_logo": "Thay đổi logo", + "logo_normal_label": "Bình thường", + "logo_dark_label": "Đảo ngược cho chế độ tối", + "configuration": "Cấu hình", + "convert_html_to_text": "Chuyển đổi HTML thành văn bản thuần", + "copy_to_clipboard": "Văn bản đã được sao chép vào bộ nhớ tạm!", + "cors_settings": "Cài đặt CORS", + "credentials_transport_warning": "Cảnh báo: Thêm một mục ánh xạ vận chuyển mới sẽ cập nhật thông tin xác thực cho tất cả các mục có cột bước nhảy tiếp theo khớp.", + "customer_id": "ID Khách hàng", + "customize": "Tùy chỉnh", + "login_page": "Trang đăng nhập", + "destination": "Đích đến", + "dkim_add_key": "Thêm khóa ARC/DKIM", + "dkim_domains_selector": "Bộ chọn", + "dkim_domains_wo_keys": "Chọn tên miền thiếu khóa", + "dkim_from": "Từ", + "dkim_from_title": "Tên miền nguồn để sao chép dữ liệu", + "dkim_key_length": "Độ dài khóa DKIM (bits)", + "dkim_key_missing": "Thiếu khóa", + "dkim_key_unused": "Khóa không được sử dụng", + "dkim_key_valid": "Khóa hợp lệ", + "dkim_keys": "Khóa ARC/DKIM", + "dkim_overwrite_key": "Ghi đè khóa DKIM hiện có", + "dkim_private_key": "Khóa riêng tư", + "dkim_to": "Đến", + "dkim_to_title": "Tên miền đích - sẽ bị ghi đè", + "domain": "Tên miền", + "domain_admin": "Quản trị viên tên miền", + "domain_admins": "Các quản trị viên tên miền", + "domainadmin_quicklink": "Ẩn liên kết nhanh đến trang đăng nhập quản trị viên tên miền", + "domain_s": "Tên miền/s", + "duplicate": "Nhân bản", + "duplicate_dkim": "Nhân bản bản ghi DKIM", + "edit": "Chỉnh sửa", + "empty": "Không có kết quả", + "excludes": "Loại trừ những người nhận này", + "f2b_ban_time": "Thời gian cấm (giây)", + "f2b_ban_time_increment": "Thời gian cấm tăng dần với mỗi lần cấm", + "f2b_blacklist": "Mạng/máy chủ bị từ chối", + "f2b_filter": "Bộ lọc biểu thức chính quy", + "f2b_list_info": "Một máy chủ hoặc mạng bị từ chối sẽ luôn có quyền ưu tiên cao hơn một thực thể trong danh sách cho phép. Cập nhật danh sách sẽ mất vài giây để được áp dụng.", + "f2b_manage_external": "Quản lý Fail2Ban từ bên ngoài", + "f2b_manage_external_info": "Fail2ban sẽ vẫn duy trì danh sách cấm, nhưng sẽ không chủ động đặt quy tắc để chặn lưu lượng. Sử dụng danh sách cấm được tạo bên dưới để chặn lưu lượng từ bên ngoài.", + "f2b_max_attempts": "Số lần thử tối đa", + "f2b_max_ban_time": "Thời gian cấm tối đa (giây)", + "f2b_netban_ipv4": "Kích thước mạng con IPv4 để áp dụng lệnh cấm (8-32)", + "f2b_netban_ipv6": "Kích thước mạng con IPv6 để áp dụng lệnh cấm (8-128", + "f2b_parameters": "Tham số Fail2ban", + "f2b_regex_info": "Nhật ký được xem xét: SOGo, Postfix, Dovecot, PHP-FPM.", + "f2b_retry_window": "Cửa sổ thử lại (giây) cho số lần thử tối đa", + "f2b_whitelist": "Mạng/máy chủ được cho phép", + "filter": "Bộ lọc", + "filter_table": "Bảng bộ lọc", + "force_sso_text": "Nếu nhà cung cấp OIDC bên ngoài được cấu hình, tùy chọn này sẽ ẩn biểu mẫu đăng nhập mailcow mặc định và chỉ hiển thị nút Đăng nhập một lần", + "force_sso": "Vô hiệu hóa đăng nhập mailcow và chỉ hiển thị Đăng nhập một lần", + "forwarding_hosts": "Máy chủ chuyển tiếp", + "forwarding_hosts_add_hint": "Bạn có thể chỉ định địa chỉ IPv4/IPv6, mạng theo ký hiệu CIDR, tên máy chủ (sẽ được phân giải thành địa chỉ IP), hoặc tên miền (sẽ được phân giải thành địa chỉ IP bằng cách truy vấn bản ghi SPF hoặc, nếu không có, bản ghi MX).", + "forwarding_hosts_hint": "Tin nhắn đến được chấp nhận vô điều kiện từ bất kỳ máy chủ nào được liệt kê ở đây. Các máy chủ này sau đó không bị kiểm tra với DNSBL hoặc danh sách xám. Thư rác nhận từ chúng không bao giờ bị từ chối, nhưng có thể được chuyển vào thư mục Rác. Công dụng phổ biến nhất cho việc này là chỉ định các máy chủ thư mà bạn đã thiết lập quy tắc chuyển tiếp email đến cho máy chủ mailcow của bạn.", + "from": "Từ", + "generate": "tạo", + "guid": "GUID - ID phiên bản duy nhất", + "guid_and_license": "GUID & Giấy phép", + "hash_remove_info": "Xóa một giá trị băm giới hạn tốc độ (nếu vẫn tồn tại) sẽ đặt lại hoàn toàn bộ đếm của nó.mailcow_password
    , được ánh xạ trong Keycloak.", + "iam_basedn": "DN cơ sở", + "iam_client_id": "ID khách hàng", + "iam_client_secret": "Bí mật khách hàng", + "iam_client_scopes": "Phạm vi khách hàng", + "iam_default_template": "Mẫu mặc định", + "iam_default_template_description": "Nếu không có mẫu nào được gán cho người dùng, mẫu mặc định sẽ được sử dụng để tạo hộp thư, nhưng không dùng để cập nhật hộp thư.", + "iam_description": "Cấu hình Nhà cung cấp bên ngoài cho Xác thực
    Hộp thư của người dùng sẽ được tự động tạo khi họ đăng nhập lần đầu, với điều kiện ánh xạ thuộc tính đã được thiết lập.", + "iam_extra_permission": "Để các cài đặt sau hoạt động, ứng dụng khách mailcow trong Keycloak cần một Tài khoản dịch vụ và quyền xem người dùng.", + "iam_host": "Máy chủ", + "iam_host_info": "Nhập một hoặc nhiều máy chủ LDAP, phân cách bằng dấu phẩy.", + "iam_import_users": "Nhập người dùng", + "iam_login_provisioning": "Tự động tạo người dùng khi đăng nhập", + "iam_mapping": "Ánh xạ thuộc tính", + "iam_bindpass": "Mật khẩu ràng buộc", + "iam_periodic_full_sync": "Đồng bộ hóa đầy đủ định kỳ", + "iam_port": "Cổng", + "iam_realm": "Vùng", + "iam_redirect_url": "URL chuyển hướng", + "iam_rest_flow": "Luồng mật khẩu thư", + "iam_server_url": "URL máy chủ", + "iam_sso": "Đăng nhập một lần", + "iam_sync_interval": "Khoảng thời gian đồng bộ / nhập (phút)", + "iam_test_connection": "Kiểm tra kết nối", + "iam_token_url": "Điểm cuối token", + "iam_userinfo_url": "Điểm cuối thông tin người dùng", + "iam_username_field": "Trường tên người dùng", + "iam_binddn": "DN ràng buộc", + "iam_use_ssl": "Sử dụng SSL", + "iam_use_ssl_info": "Nếu bật SSL và cổng được đặt là 389, nó sẽ tự động được ghi đè để sử dụng cổng 636.", + "iam_use_tls": "Sử dụng StartTLS", + "iam_use_tls_info": "Nếu bật TLS, bạn phải sử dụng cổng mặc định cho máy chủ LDAP của bạn (389). Không thể sử dụng các cổng SSL.", + "iam_version": "Phiên bản", + "ignore_ssl_error": "Bỏ qua lỗi SSL", + "import": "Nhập", + "import_private_key": "Nhập khóa riêng tư", + "in_use_by": "Đang được sử dụng bởi", + "inactive": "Không hoạt động", + "include_exclude": "Bao gồm/Loại trừ", + "include_exclude_info": "Mặc định - khi không có lựa chọn - tất cả hộp thư được đề cập", + "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.emailipv6.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.", + "link": "Liên kết", + "loading": "Vui lòng đợi...", + "login_time": "Thời gian đăng nhập", + "logo_info": "Hình ảnh của bạn sẽ được điều chỉnh theo chiều cao 40px cho thanh điều hướng trên cùng và chiều rộng tối đa 250px cho trang bắt đầu. Khuyến nghị sử dụng đồ họa có thể thay đổi kích thước.", + "lookup_mx": "Đích đến là một biểu thức chính quy để so 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)", + "main_name": "Tên \"Giao diện mailcow\"", + "merged_vars_hint": "Các hàng bị mờ đã được hợp nhất từ vars.(local.)inc.php và không thể sửa đổi.", + "message": "Tin nhắn", + "message_size": "Kích thước tin nhắn", + "nexthop": "Bước nhảy tiếp theo", + "needs_restart": "cần khởi động lại", + "no_active_bans": "Không có lệnh cấm đang hoạt động", + "no_new_rows": "Không có hàng nào khác", + "no_record": "Không có bản ghi", + "oauth2_apps": "Ứng dụng OAuth2", + "oauth2_add_client": "Thêm ứng dụng khách OAuth2", + "oauth2_client_id": "ID ứng dụng khách", + "oauth2_client_secret": "Bí mật ứng dụng khách", + "oauth2_info": "Triển khai OAuth2 hỗ trợ loại cấp phép \"Mã ủy quyền\" và cấp token làm mới.
    \nMáy chủ cũng tự động cấp token làm mới mới sau khi token làm mới đã được sử dụng.

    \n• Phạm vi mặc định là profile. Chỉ người dùng hộp thư mới có thể được xác thực qua OAuth2. Nếu tham số phạm vi bị bỏ qua, nó sẽ trở về profile.
    \n• Tham số state phải được gửi bởi ứng dụng khách như một phần của yêu cầu ủy quyền.

    \nĐường dẫn cho các yêu cầu đến API OAuth2:
    \n
      \n
    • Điểm cuối ủy quyền: /oauth/authorize
    • \n
    • Điểm cuối token: /oauth/token
    • \n
    • Trang tài nguyên: /oauth/profile
    • \n
    \nTạo lại bí mật ứng dụng khách sẽ không làm hết hạn các mã ủy quyền hiện có, nhưng chúng sẽ không thể làm mới token của họ.

    \nThu hồi token ứng dụng khách sẽ gây ra việc chấm dứt ngay lập tức tất cả các phiên hoạt động. Tất cả ứng dụng khách cần xác thực lại.", + "oauth2_redirect_uri": "URI chuyển hướng", + "oauth2_renew_secret": "Tạo bí mật ứng dụng khách mới", + "oauth2_revoke_tokens": "Thu hồi tất cả token ứng dụng khách", + "optional": "tùy chọn", + "options": "Các tùy chọn", + "password": "Mật khẩu", + "password_length": "Độ dài mật khẩu", + "password_policy": "Chính sách mật khẩu", + "password_policy_chars": "Phải chứa ít nhất một ký tự chữ cái", + "password_policy_length": "Độ dài mật khẩu tối thiểu là %d", + "password_policy_lowerupper": "Phải chứa ký tự viết thường và viết hoa", + "password_policy_numbers": "Phải chứa ít nhất một số", + "password_policy_special_chars": "Phải chứa ký tự đặc biệt", + "password_repeat": "Xác nhận mật khẩu (nhập lại)", + "password_reset_info": "Nếu không cung cấp email khôi phục, chức năng này không thể được sử dụng.", + "password_reset_settings": "Cài đặt khôi phục mật khẩu", + "password_reset_tmpl_html": "Mẫu HTML", + "password_reset_tmpl_text": "Mẫu văn bản", + "password_settings": "Cài đặt mật khẩu", + "priority": "Độ ưu tiên", + "private_key": "Khóa riêng tư", + "quarantine": "Cách ly", + "quarantine_bcc": "Gửi một bản sao của tất cả thông báo (BCC) đến người nhận này:
    Để trống để vô hiệu hóa. Thư không ký, không kiểm tra. Chỉ nên gửi nội bộ.", + "quarantine_exclude_domains": "Loại trừ tên miền và tên miền bí danh", + "quarantine_max_age": "Tuổi tối đa theo ngày
    Giá trị phải bằng hoặc lớn hơn 1 ngày.", + "quarantine_max_score": "Bỏ thông báo nếu điểm thư rác của một thư cao hơn giá trị này:
    Mặc định là 9999.0", + "quarantine_max_size": "Kích thước tối đa tính bằng MiB (các phần tử lớn hơn sẽ bị loại bỏ):
    0 không có nghĩa là không giới hạn.", + "quarantine_notification_html": "Mẫu email thông báo:
    Để trống để khôi phục mẫu mặc định.", + "quarantine_notification_sender": "Người gửi email thông báo", + "quarantine_notification_subject": "Chủ đề email thông báo", + "quarantine_redirect": "Chuyển hướng tất cả thông báo đến người nhận này:
    Để trống để vô hiệu hóa. Thư không ký, không kiểm tra. Chỉ nên gửi nội bộ.", + "quarantine_release_format": "Định dạng của các mục được phát hành", + "quarantine_release_format_att": "Dưới dạng tệp đính kèm", + "quarantine_release_format_raw": "Bản gốc không sửa đổi", + "quarantine_retention_size": "ưu giữ cho mỗi hộp thư:
    0 có nghĩa là không hoạt động.", + "quicklink_text": "Hiển thị hoặc ẩn liên kết nhanh đến các trang đăng nhập khác dưới biểu mẫu đăng nhập", + "quota_notification_html": "Mẫu email thông báo:
    Để trống để khôi phục mẫu mặc định.", + "quota_notification_sender": "Người gửi email thông báo", + "quota_notification_subject": "Chủ đề email thông báo", + "quota_notifications": "Thông báo hạn ngạch", + "quota_notifications_info": "Thông báo hạn ngạch được gửi cho người dùng một lần khi vượt quá 80% và một lần khi vượt quá 95% mức sử dụng.", + "quota_notifications_vars": "{{percent}} bằng hạn ngạch hiện tại của người dùng
    {{username}} là tên hộp thư", + "queue_unban": "bỏ cấm", + "r_active": "Hạn chế đang hoạt động", + "r_inactive": "Hạn chế không hoạt động", + "r_info": "Các phần tử bị mờ/vô hiệu hóa trong danh sách hạn chế đang hoạt động không được mailcow công nhận là hạn chế hợp lệ và không thể di chuyển. Các hạn chế không xác định sẽ vẫn được đặt theo thứ tự xuất hiện.
    Bạn có thể thêm phần tử mới trong inc/vars.local.inc.php để có thể bật/tắt chúng.", + "rate_name": "Tên tỷ lệ", + "recipients": "Người nhận", + "refresh": "Làm mới", + "regen_api_key": "Tạo lại khóa API", + "regex_maps": "Ánh xạ biểu thức chính quy", + "relay_from": "Địa chỉ \"Từ:\"", + "relay_rcpt": "Địa chỉ \"Đến:\"", + "relay_run": "Chạy thử", + "relayhosts": "Vận chuyển phụ thuộc người gửi", + "relayhosts_hint": "Xác định vận chuyển phụ thuộc người gửi để có thể chọn chúng trong hộp thoại cấu hình tên miền.
    \nDịch vụ vận chuyển luôn là \"smtp:\" và do đó sẽ thử TLS khi được cung cấp. TLS được bọc (SMTPS) không được hỗ trợ. Cài đặt chính sách TLS gửi đi riêng của người dùng được tính đến.
    \nẢnh hưởng đến các tên miền được chọn bao gồm cả tên miền bí danh.", + "remove": "Xóa", + "remove_row": "Xóa hàng", + "reset_default": "Đặt lại về mặc định", + "reset_limit": "Xóa giá trị băm", + "reset_password_vars": "{{link}} Liên kết đặt lại mật khẩu đã tạo
    {{username}} Tên hộp thư của người dùng yêu cầu đặt lại mật khẩu
    {{username2}} Tên hộp thư khôi phục
    {{date}} Ngày yêu cầu đặt lại mật khẩu được thực hiện
    {{token_lifetime}} Thời gian sống của token tính bằng phút
    {{hostname}} Tên máy chủ mailcow", + "restore_template": "Để trống để khôi phục mẫu mặc định.", + "routing": "Định tuyến", + "rsetting_add_rule": "Thêm quy tắc", + "rsetting_content": "Nội dung quy tắc", + "rsetting_desc": "Mô tả ngắn", + "rsetting_no_selection": "Vui lòng chọn một quy tắc", + "rsetting_none": "Không có quy tắc nào", + "rsettings_insert_preset": "Chèn mẫu ví dụ \"%s\"", + "rsettings_preset_1": "Vô hiệu hóa tất cả trừ DKIM và giới hạn tốc độ cho người dùng đã xác thực", + "rsettings_preset_2": "Quản trị viên bưu điện muốn thư rác", + "rsettings_preset_3": "Chỉ cho phép người gửi cụ thể cho một hộp thư (ví dụ: sử dụng như hộp thư nội bộ)", + "rsettings_preset_4": "Vô hiệu hóa Rspamd cho một tên miền", + "rspamd_com_settings": "Tên cài đặt sẽ được tự động tạo, vui lòng xem các mẫu ví dụ bên dưới. Để biết thêm chi tiết, xem Tài liệu Rspamd", + "rspamd_global_filters": "Ánh xạ bộ lọc toàn cục", + "rspamd_global_filters_agree": "Tôi sẽ cẩn thận!", + "rspamd_global_filters_info": "Ánh xạ bộ lọc toàn cục chứa các loại danh sách từ chối và cho phép toàn cục khác nhau.", + "rspamd_global_filters_regex": "Tên của chúng giải thích mục đích sử dụng. Tất cả nội dung phải chứa biểu thức chính quy hợp lệ theo định dạng \"/mẫu/tùy chọn\" (ví dụ: /.+@domain.tld/i).
    \nMặc dù kiểm tra cơ bản được thực hiện trên mỗi dòng của regex, chức năng của Rspamd có thể bị hỏng nếu nó không đọc được cú pháp chính xác.
    \nRspamd sẽ cố gắng đọc nội dung ánh xạ khi thay đổi. Nếu bạn gặp vấn đề, khởi động lại Rspamd để bắt buộc tải lại ánh xạ.
    Các phần tử trong danh sách từ chối được loại trừ khỏi cách ly.", + "rspamd_settings_map": "Ánh xạ cài đặt Rspamd", + "sal_level": "Cấp độ Moo", + "save": "Lưu thay đổi", + "search_domain_da": "Tìm kiếm tên miền", + "send": "Gửi", + "sender": "Người gửi", + "service": "Dịch vụ", + "service_id": "ID Dịch vụ", + "source": "Nguồn", + "spamfilter": "Bộ lọc thư rác", + "subject": "Chủ đề", + "success": "Thành công", + "sys_mails": "Thư hệ thống", + "task": "Nhiệm vụ", + "text": "Văn bản", + "time": "Thời gian", + "title": "Tiêu đề", + "title_name": "Tiêu đề trang web \"Giao diện mailcow\"", + "to_top": "Về đầu trang", + "transport_dest_format": "Regex hoặc cú pháp: example.org, .example.org, *, box@example.org (nhiều giá trị có thể được phân cách bằng dấu phẩy)", + "transport_maps": "Ánh xạ vận chuyển", + "transport_test_rcpt_info": "• Sử dụng null@hosted.mailcow.de để kiểm tra chuyển tiếp đến đích nước ngoài.", + "transports_hint": "• Một mục ánh xạ vận chuyển ghi đè một ánh xạ vận chuyển phụ thuộc người gửi.
    \n• Vận chuyển dựa trên MX được ưu tiên sử dụng.
    \n• Cài đặt chính sách TLS gửi đi cho từng người dùng bị bỏ qua và chỉ có thể được thực thi bởi các mục ánh xạ chính sách TLS.
    \n• Dịch vụ vận chuyển cho các vận chuyển đã định nghĩa luôn là \"smtp:\" và do đó sẽ thử TLS khi được cung cấp. TLS được bọc (SMTPS) không được hỗ trợ.
    \n• Địa chỉ khớp với \"/localhost$/\" sẽ luôn được vận chuyển qua \"local:\", do đó đích \"*\" sẽ không áp dụng cho những địa chỉ đó.
    \n• Để xác định thông tin xác thực cho bước nhảy tiếp theo ví dụ \"[host]:25\", Postfix luôn truy vấn \"host\" trước khi tìm kiếm \"[host]:25\". Hành vi này khiến không thể sử dụng \"host\" và \"[host]:25\" cùng một lúc.", + "ui_footer": "Chân trang (cho phép HTML)", + "ui_header_announcement": "Thông báo", + "ui_header_announcement_active": "Đặt thông báo hoạt động", + "ui_header_announcement_content": "Văn bản (cho phép HTML)", + "ui_header_announcement_help": "Thông báo hiển thị cho tất cả người dùng đã đăng nhập và trên màn hình đăng nhập của giao diện người dùng.", + "ui_header_announcement_select": "Chọn loại thông báo", + "ui_header_announcement_type": "Loại", + "ui_header_announcement_type_danger": "Rất quan trọng", + "ui_header_announcement_type_info": "Thông tin", + "ui_header_announcement_type_warning": "Quan trọng", + "ui_texts": "Nhãn và văn bản giao diện người dùng", + "unban_pending": "đang chờ bỏ cấm", + "unchanged_if_empty": "Nếu không thay đổi hãy để trống", + "upload": "Tải lên", + "username": "Tên người dùng", + "user_link": "Liên kết người dùng", + "user_quicklink": "Ẩn liên kết nhanh đến trang đăng nhập người dùng", + "validate_license_now": "Xác thực GUID với máy chủ giấy phép", + "verify": "Xác minh", + "yes": "✓" + }, + "danger": { + "access_denied": "Truy cập bị từ chối hoặc dữ liệu biểu mẫu không hợp lệ", + "alias_domain_invalid": "Tên miền bí danh %s không hợp lệ", + "alias_empty": "Địa chỉ bí danh không được để trống", + "alias_goto_identical": "Địa chỉ bí danh và địa chỉ chuyển tiếp không được giống nhau", + "alias_invalid": "Địa chỉ bí danh %s không hợp lệ", + "aliasd_targetd_identical": "Tên miền bí danh không được giống với tên miền đích: %s", + "aliases_in_use": "Số bí danh tối đa phải lớn hơn hoặc bằng %d", + "app_name_empty": "Tên ứng dụng không được để trống", + "app_passwd_id_invalid": "ID mật khẩu ứng dụng %s không hợp lệ", + "authsource_in_use": "Nhà cung cấp danh tính không thể thay đổi hoặc xóa vì hiện đang được sử dụng bởi một hoặc nhiều người dùng.", + "bcc_empty": "Đích BCC không được để trống", + "bcc_exists": "Một ánh xạ BCC %s đã tồn tại cho loại %s", + "bcc_must_be_email": "Đích BCC %s không phải là địa chỉ email hợp lệ", + "comment_too_long": "Bình luận quá dài, cho phép tối đa 160 ký tự", + "cors_invalid_method": "Phương thức Allow-Method được chỉ định không hợp lệ", + "cors_invalid_origin": "Allow-Origin được chỉ định không hợp lệ", + "defquota_empty": "Hạn ngạch mặc định cho mỗi hộp thư không được là 0.", + "demo_mode_enabled": "Chế độ Demo đang được bật", + "description_invalid": "Mô tả tài nguyên cho %s không hợp lệ", + "dkim_domain_or_sel_exists": "Một khóa DKIM cho \"%s\" đã tồn tại và sẽ không bị ghi đè", + "dkim_domain_or_sel_invalid": "Tên miền hoặc bộ chọn DKIM không hợp lệ: %s", + "domain_cannot_match_hostname": "Tên miền không thể trùng với tên máy chủ", + "domain_exists": "Tên miền %s đã tồn tại", + "domain_invalid": "Tên miền trống hoặc không hợp lệ", + "domain_not_empty": "Không thể xóa tên miền %s không trống", + "domain_not_found": "Không tìm thấy tên miền %s", + "domain_quota_m_in_use": "Hạn ngạch tên miền phải lớn hơn hoặc bằng %s MiB", + "extended_sender_acl_denied": "thiếu ACL để đặt địa chỉ người gửi bên ngoài", + "extra_acl_invalid": "Địa chỉ người gửi bên ngoài \"%s\" không hợp lệ", + "extra_acl_invalid_domain": "Người gửi bên ngoài \"%s\" sử dụng tên miền không hợp lệ", + "fido2_verification_failed": "Xác minh FIDO2 thất bại: %s", + "file_open_error": "Không thể mở tệp để ghi", + "filter_type": "Loại bộ lọc không đúng", + "from_invalid": "Người gửi không được để trống", + "generic_server_error": "Đã xảy ra lỗi máy chủ không mong đợi. Vui lòng liên hệ quản trị viên của bạn.", + "global_filter_write_error": "Không thể ghi tệp bộ lọc: %s", + "global_map_invalid": "ID ánh xạ toàn cục %s không hợp lệ", + "global_map_write_error": "Không thể ghi ID ánh xạ toàn cục %s: %s", + "goto_empty": "Một địa chỉ bí danh phải chứa ít nhất một địa chỉ chuyển tiếp hợp lệ", + "goto_invalid": "Địa chỉ chuyển tiếp %s không hợp lệ", + "ham_learn_error": "Lỗi học thư hợp lệ: %s", + "iam_test_connection": "Kết nối thất bại", + "imagick_exception": "Lỗi: Ngoại lệ Imagick khi đọc hình ảnh", + "img_dimensions_exceeded": "Hình ảnh vượt quá kích thước tối đa cho phép", + "img_invalid": "Không thể xác thực tệp hình ảnh", + "img_size_exceeded": "Hình ảnh vượt quá kích thước tệp tối đa", + "img_tmp_missing": "Không thể xác thực tệp hình ảnh: Không tìm thấy tệp tạm thời", + "invalid_bcc_map_type": "Loại ánh xạ BCC không hợp lệ", + "invalid_destination": "Định dạng đích \"%s\" không hợp lệ", + "invalid_filter_type": "Loại bộ lọc không hợp lệ", + "invalid_host": "Máy chủ được chỉ định không hợp lệ: %s", + "invalid_mime_type": "Kiểu MIME không hợp lệ", + "invalid_nexthop": "Định dạng bước nhảy tiếp theo không hợp lệ", + "invalid_nexthop_authenticated": "Bước nhảy tiếp theo tồn tại với thông tin xác thực khác, vui lòng cập nhật thông tin xác thực hiện có cho bước nhảy này trước.", + "invalid_recipient_map_new": "Người nhận mới được chỉ định không hợp lệ: %s", + "invalid_recipient_map_old": "Người nhận gốc được chỉ định không hợp lệ: %s", + "invalid_reset_token": "Token đặt lại không hợp lệ", + "ip_list_empty": "Danh sách IP được phép không được để trống", + "is_alias": "%s đã được biết đến như một địa chỉ bí danh", + "is_alias_or_mailbox": "%s đã được biết đến như một bí danh, một hộp thư hoặc một địa chỉ bí danh được mở rộng từ một tên miền bí danh.", + "is_spam_alias": "%s đã được biết đến như một địa chỉ bí danh tạm thời (địa chỉ bí danh thư rác)", + "last_key": "Không thể xóa khóa cuối cùng, vui lòng vô hiệu hóa TFA thay thế.", + "login_failed": "Đăng nhập không thành công", + "mailbox_defquota_exceeds_mailbox_maxquota": "Hạn ngạch mặc định vượt quá giới hạn hạn ngạch tối đa", + "mailbox_invalid": "Tên hộp thư không hợp lệ", + "mailbox_quota_exceeded": "Hạn ngạch vượt quá giới hạn tên miền (tối đa %d MiB)", + "mailbox_quota_exceeds_domain_quota": "Hạn ngạch tối đa vượt quá giới hạn hạn ngạch tên miền", + "mailbox_quota_left_exceeded": "Không đủ dung lượng còn lại (dung lượng còn lại: %d MiB)", + "mailboxes_in_use": "Số hộp thư tối đa phải lớn hơn hoặc bằng %d", + "malformed_username": "Tên người dùng không đúng định dạng", + "map_content_empty": "Nội dung ánh xạ không được để trống", + "max_age_invalid": "Tuổi tối đa %s không hợp lệ", + "max_alias_exceeded": "Vượt quá số bí danh tối đa", + "max_mailbox_exceeded": "Vượt quá số hộp thư tối đa (%d trên %d)", + "max_quota_in_use": "Hạn ngạch hộp thư phải lớn hơn hoặc bằng %d MiB", + "maxquota_empty": "Hạn ngạch tối đa cho mỗi hộp thư không được là 0.", + "mode_invalid": "Chế độ %s không hợp lệ", + "mx_invalid": "Bản ghi MX %s không hợp lệ", + "mysql_error": "Lỗi MySQL: %s", + "network_host_invalid": "Mạng hoặc máy chủ không hợp lệ: %s", + "next_hop_interferes": "%s xung đột với bước nhảy tiếp theo %s", + "next_hop_interferes_any": "Một bước nhảy tiếp theo hiện có xung đột với %s", + "nginx_reload_failed": "Tải lại Nginx thất bại: %s", + "no_user_defined": "Không có người dùng được định nghĩa", + "object_exists": "Đối tượng %s đã tồn tại", + "object_is_not_numeric": "Giá trị %s không phải là số", + "password_complexity": "Mật khẩu không đáp ứng chính sách", + "password_empty": "Mật khẩu không được để trống", + "password_mismatch": "Mật khẩu xác nhận không khớp", + "password_reset_invalid_user": "Không tìm thấy hộp thư hoặc không có email khôi phục được thiết lập", + "password_reset_na": "Tính năng khôi phục mật khẩu hiện không khả dụng. Vui lòng liên hệ quản trị viên của bạn.", + "policy_list_from_exists": "Một bản ghi với tên đã cho tồn tại", + "policy_list_from_invalid": "Bản ghi có định dạng không hợp lệ", + "private_key_error": "Lỗi khóa riêng tư: %s", + "pushover_credentials_missing": "Thiếu token hoặc khóa Pushover", + "pushover_key": "Khóa Pushover có định dạng không đúng", + "pushover_token": "Token Pushover có định dạng không đúng", + "quota_not_0_not_numeric": "Hạn ngạch phải là số và >= 0", + "recipient_map_entry_exists": "Mục ánh xạ người nhận \"%s\" đã tồn tại", + "recovery_email_failed": "Không thể gửi email khôi phục. Vui lòng liên hệ quản trị viên của bạn.", + "redis_error": "Lỗi Redis: %s", + "relayhost_invalid": "Mục ánh xạ %s không hợp lệ", + "release_send_failed": "Không thể phát hành thư: %s", + "required_data_missing": "Thiếu dữ liệu bắt buộc %s", + "reset_f2b_regex": "Không thể đặt lại bộ lọc biểu thức chính quy kịp thời, vui lòng thử lại hoặc đợi thêm vài giây và tải lại trang web.", + "reset_token_limit_exceeded": "Đã vượt quá giới hạn token đặt lại. Vui lòng thử lại sau.", + "resource_invalid": "Tên tài nguyên %s không hợp lệ", + "rl_timeframe": "Khung thời gian giới hạn tốc độ không chính xác", + "rspamd_ui_pw_length": "Mật khẩu giao diện Rspamd phải có ít nhất 6 ký tự", + "script_empty": "Tập lệnh không được để trống", + "sender_acl_invalid": "Giá trị ACL người gửi %s không hợp lệ", + "set_acl_failed": "Không thể thiết lập ACL", + "settings_map_invalid": "ID ánh xạ cài đặt %s không hợp lệ", + "sieve_error": "Lỗi phân tích cú pháp Sieve: %s", + "spam_learn_error": "Lỗi học thư rác: %s", + "subject_empty": "Tiêu đề không được để trống", + "target_domain_invalid": "Tên miền đích %s không hợp lệ", + "targetd_not_found": "Không tìm thấy tên miền đích %s", + "targetd_relay_domain": "Tên miền đích %s là tên miền chuyển tiếp", + "template_exists": "Mẫu %s đã tồn tại", + "template_id_invalid": "ID mẫu %s không hợp lệ", + "template_name_invalid": "Tên mẫu không hợp lệ", + "temp_error": "Lỗi tạm thời", + "text_empty": "Văn bản không được để trống", + "tfa_token_invalid": "Token xác thực hai yếu tố không hợp lệ", + "tls_policy_map_dest_invalid": "Đích chính sách TLS không hợp lệ", + "tls_policy_map_entry_exists": "Mục ánh xạ chính sách TLS \"%s\" đã tồn tại", + "tls_policy_map_parameter_invalid": "Tham số chính sách không hợp lệ", + "to_invalid": "Người nhận không được để trống", + "totp_verification_failed": "Xác thực TOTP thất bại", + "transport_dest_exists": "Đích vận chuyển \"%s\" đã tồn tại", + "webauthn_verification_failed": "Xác thực WebAuthn thất bại: %s", + "webauthn_authenticator_failed": "Không tìm thấy trình xác thực đã chọn", + "webauthn_publickey_failed": "Không có khóa công khai nào được lưu cho trình xác thực đã chọn", + "webauthn_username_failed": "Trình xác thực đã chọn thuộc về tài khoản khác", + "unknown": "Đã xảy ra lỗi không xác định", + "unknown_tfa_method": "Phương thức xác thực hai yếu tố không xác định", + "unlimited_quota_acl": "Hạn ngạch không giới hạn bị cấm bởi ACL", + "username_invalid": "Tên người dùng %s không thể sử dụng được", + "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" + }, + "datatables": { + "collapse_all": "Thu gọn tất cả", + "emptyTable": "Không có dữ liệu trong bảng", + "expand_all": "Mở rộng tất cả", + "info": "Hiển thị từ _START_ đến _END_ của _TOTAL_ mục", + "infoEmpty": "Hiển thị 0 đến 0 của 0 mục", + "infoFiltered": "(được lọc từ tổng số _MAX_ mục)", + "lengthMenu": "Hiển thị _MENU_ mục", + "loadingRecords": "Đang tải...", + "processing": "Vui lòng đợi...", + "search": "Tìm kiếm:", + "zeroRecords": "Không tìm thấy bản ghi phù hợp", + "paginate": { + "first": "Đầu", + "last": "Cuối", + "next": "Tiếp", + "previous": "Trước" + }, + "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" + } + }, + "debug": { + "architecture": "Kiến trúc", + "chart_this_server": "Biểu đồ (máy chủ này)", + "containers_info": "Thông tin container", + "container_running": "Đang chạy", + "container_disabled": "Container đã dừng hoặc bị vô hiệu hóa", + "container_stopped": "Đã dừng", + "cores": "Nhân CPU", + "current_time": "Thời gian hệ thống", + "disk_usage": "Sử dụng ổ đĩa", + "docs": "Tài liệu", + "error_show_ip": "Không thể phân giải địa chỉ IP công khai", + "external_logs": "Nhật ký ngoài", + "history_all_servers": "Lịch sử (tất cả máy chủ)", + "in_memory_logs": "Nhật ký trong bộ nhớ", + "last_modified": "Sửa đổi lần cuối", + "log_info": "

    mailcow nhật ký trong bộ nhớ được thu thập trong danh sách Redis và được cắt giảm xuống LOG_LINES (%d) mỗi phút để giảm tải.\n
    Nhật ký trong bộ nhớ không nhằm mục đích lưu trữ lâu dài. Tất cả các ứng dụng ghi nhật ký trong bộ nhớ cũng ghi vào Docker daemon và do đó vào trình điều khiển ghi nhật ký mặc định.r\n
    Loại nhật ký trong bộ nhớ nên được sử dụng để gỡ lỗi các vấn đề nhỏ với các container.

    \n

    Nhật ký bên ngoài được thu thập thông qua API của ứng dụng đã cho.

    \n

    Nhật ký tĩnh chủ yếu là nhật ký hoạt động, không được ghi vào Dockerd nhưng vẫn cần được lưu trữ lâu dài (ngoại trừ nhật ký API).

    ", + "login_time": "Thời gian", + "logs": "Nhật ký", + "memory": "Bộ nhớ", + "online_users": "Người dùng trực tuyến", + "restart_container": "Khởi động lại", + "service": "Dịch vụ", + "show_ip": "Hiển thị IP công khai", + "size": "Kích thước", + "started_at": "Bắt đầu lúc", + "started_on": "Bắt đầu vào", + "static_logs": "Nhật ký tĩnh", + "success": "Thành công", + "system_containers": "Hệ thống & Container", + "timezone": "Múi giờ", + "uptime": "Thời gian hoạt động", + "update_available": "Có bản cập nhật mới", + "no_update_available": "Hệ thống đang ở phiên bản mới nhất", + "update_failed": "Không thể kiểm tra cập nhật", + "username": "Tên người dùng", + "wip": "Đang trong quá trình phát triển" + }, + "diagnostics": { + "cname_from_a": "Giá trị được lấy từ bản ghi A/AAAA. Điều này được hỗ trợ miễn là bản ghi trỏ đến tài nguyên chính xác.", + "dns_records": "Bản ghi DNS", + "dns_records_24hours": "Xin lưu ý rằng các thay đổi được thực hiện đối với DNS có thể mất tới 24 giờ để phản ánh chính xác trạng thái hiện tại của chúng trên trang này. Trang này nhằm giúp bạn dễ dàng xem cách cấu hình bản ghi DNS và kiểm tra xem tất cả bản ghi của bạn có được lưu trữ chính xác trong DNS hay không.", + "dns_records_data": "Dữ liệu chính xác", + "dns_records_docs": "Vui lòng tham khảo thêm tài liệu hướng dẫn.", + "dns_records_name": "Tên", + "dns_records_status": "Trạng thái hiện tại", + "dns_records_type": "Loại", + "optional": "Bản ghi này là tùy chọn." + }, + "edit": { + "acl": "ACL (Quyền hạn)", + "active": "Hoạt động", + "admin": "Chỉnh sửa quản trị viên", + "advanced_settings": "Cài đặt nâng cao", + "alias": "Chỉnh sửa bí danh", + "allow_from_smtp": "Chỉ cho phép các IP sau sử dụng SMTP", + "allow_from_smtp_info": "Để trống để cho phép tất cả người gửi.
    Địa chỉ và mạng IPv4/IPv6.", + "allowed_protocols": "Các giao thức được phép truy cập trực tiếp của người dùng (không ảnh hưởng đến giao thức mật khẩu ứng dụng)", + "app_name": "Tên ứng dụng", + "app_passwd": "Mật khẩu ứng dụng", + "app_passwd_protocols": "Các giao thức được phép cho mật khẩu ứng dụng", + "automap": "Thử tự động ánh xạ thư mục (\"Mục đã gửi\", \"Đã gửi\" => \"Đã gửi\" v.v.)", + "backup_mx_options": "Tùy chọn chuyển tiếp", + "bcc_dest_format": "Đích BCC phải là một địa chỉ email hợp lệ duy nhất.
    Nếu bạn cần gửi bản sao đến nhiều địa chỉ, hãy tạo một bí danh và sử dụng nó ở đây.", + "client_id": "ID khách hàng", + "client_secret": "Khóa bí mật khách hàng", + "comment_info": "Bình luận riêng tư không hiển thị với người dùng, trong khi bình luận công khai được hiển thị dưới dạng chú thích khi di chuột qua trong tổng quan người dùng", + "created_on": "Được tạo vào", + "custom_attributes": "Thuộc tính tùy chỉnh", + "delete1": "Xóa từ nguồn khi hoàn thành", + "delete2": "Xóa thư ở đích không có ở nguồn", + "delete2duplicates": "Xóa các bản sao ở đích", + "delete_ays": "Vui lòng xác nhận quá trình xóa.", + "description": "Mô tả", + "disable_login": "Không cho phép đăng nhập (vẫn nhận được thư đến)", + "domain": "Chỉnh sửa tên miền", + "domain_admin": "Chỉnh sửa quản trị viên tên miền", + "domain_footer": "Chân trang toàn tên miền", + "domain_footer_html": "Chân trang HTML", + "domain_footer_info": "Chân trang toàn tên miền được thêm vào tất cả các email gửi đi liên quan đến một địa chỉ trong tên miền này.
    Các biến sau có thể được sử dụng cho chân trang:", + "domain_footer_info_vars": { + "auth_user": "{= auth_user =} - Tên người dùng đã xác thực được chỉ định bởi MTA", + "from_user": "{= from_user =} - Phần người dùng của phong bì, ví dụ \"moo@mailcow.tld\" sẽ trả về \"moo\"", + "from_name": "{= from_name =} - Tên từ phong bì, ví dụ \"Mailcow <moo@mailcow.tld>\" sẽ trả về \"Mailcow\"", + "from_addr": "{= from_addr =} - Phần địa chỉ của phong bì", + "from_domain": "{= from_domain =} - Phần tên miền của phong bì", + "custom": "{= foo =} - Nếu hộp thư có thuộc tính tùy chỉnh \"foo\" với giá trị \"bar\" sẽ trả về \"bar\"" + }, + "domain_footer_plain": "Chân trang văn bản thuần", + "domain_footer_skip_replies": "Bỏ qua chân trang trong email trả lời", + "domain_quota": "Hạn ngạch tên miền", + "domains": "Các tên miền", + "dont_check_sender_acl": "Tắt kiểm tra người gửi cho tên miền %s (+ tên miền bí danh)", + "edit_alias_domain": "Chỉnh sửa tên miền bí danh", + "encryption": "Mã hóa", + "exclude": "Loại trừ đối tượng (biểu thức chính quy)", + "extended_sender_acl": "Địa chỉ người gửi bên ngoài", + "extended_sender_acl_info": "Khóa tên miền DKIM nên được nhập vào, nếu có sẵn.
    \n Nhớ thêm máy chủ này vào bản ghi SPF TXT tương ứng.
    \n Khi một tên miền hoặc tên miền bí danh được thêm vào máy chủ này, mà trùng với một địa chỉ bên ngoài, địa chỉ bên ngoài sẽ bị xóa.
    \n Sử dụng @domain.tld để cho phép gửi dưới dạng *@domain.tld.", + "force_pw_update": "Bắt buộc cập nhật mật khẩu trong lần đăng nhập tiếp theo", + "force_pw_update_info": "Người dùng này sẽ chỉ có thể đăng nhập vào %s. Mật khẩu ứng dụng vẫn có thể sử dụng được.", + "footer_exclude": "Loại trừ khỏi chân trang", + "full_name": "Tên đầy đủ", + "gal": "Danh sách địa chỉ toàn cục", + "gal_info": "GAL chứa tất cả các đối tượng của một tên miền và không thể được chỉnh sửa bởi bất kỳ người dùng nào. Thông tin rảnh/bận trong SOGo sẽ bị thiếu nếu vô hiệu hóa! Khởi động lại SOGo để áp dụng thay đổi.", + "generate": "tạo", + "grant_types": "Các loại cấp quyền", + "hostname": "Tên máy chủ", + "inactive": "Không hoạt động", + "internal": "Nội bộ", + "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)" + } +} diff --git a/mailcow/data/web/lang/lang.zh-cn.json b/mailcow/data/web/lang/lang.zh-cn.json index 70a48a9..94473a4 100644 --- a/mailcow/data/web/lang/lang.zh-cn.json +++ b/mailcow/data/web/lang/lang.zh-cn.json @@ -24,7 +24,7 @@ "sogo_access": "允许管理 SOGo 访问权限", "sogo_profile_reset": "重置 SOGo 个人资料", "spam_alias": "临时别名", - "spam_policy": "黑名单/白名单", + "spam_policy": "阻止名单/允许名单", "spam_score": "垃圾邮件分数", "syncjobs": "同步任务", "tls_policy": "TLS 策略", @@ -108,7 +108,10 @@ "timeout2": "本地主机连接超时时间", "username": "用户名", "validate": "验证", - "validation_success": "验证成功" + "validation_success": "验证成功", + "dry": "模拟同步(Dry run)", + "internal_info": "内部的别名只能在域内部或者别名域内部访问。", + "internal": "内部的" }, "admin": { "access": "权限管理", @@ -146,7 +149,7 @@ "arrival_time": "到达时间 (服务器时间)", "authed_user": "已认证用户", "ays": "确定继续操作?", - "ban_list_info": "以下为被封禁的 IP 列表: 网络 (剩余封禁时间) - [操作]
    被取消封禁的 IP 将会在几秒之内从封禁列表中移除
    红色标签表示因黑名单而导致的永久封禁。", + "ban_list_info": "以下为被封禁的 IP 列表: 网络 (剩余封禁时间) - [操作]
    被取消封禁的 IP 将会在几秒之内从封禁列表中移除
    红色标签表示因阻止名单而导致的永久封禁。", "change_logo": "更改 Logo", "configuration": "配置", "convert_html_to_text": "将 HTML 转换为纯文本内容", @@ -178,16 +181,16 @@ "empty": "结果为空", "excludes": "除了", "f2b_ban_time": "封禁时间 (秒)", - "f2b_blacklist": "网络/主机黑名单", + "f2b_blacklist": "网络/主机阻止名单", "f2b_filter": "正则表达式过滤器", - "f2b_list_info": "黑名单的优先级总是高于白名单。 列表更新将会在几秒之后完成。", + "f2b_list_info": "阻止名单的优先级总是高于允许名单。 列表更新将会在几秒之后完成。", "f2b_max_attempts": "最多尝试次数", "f2b_netban_ipv4": "应用封禁的 IPv4 子网大小 (8-32)", "f2b_netban_ipv6": "应用封禁的 IPv6 子网大小 (8-128)", "f2b_parameters": "Fail2ban 参数", "f2b_regex_info": "将会过滤这些应用的日志: SOGo,Postfix,Dovecot 和 PHP-FPM。", "f2b_retry_window": "最多尝试次数重试窗口 (秒)", - "f2b_whitelist": "网络/主机白名单", + "f2b_whitelist": "网络/主机允许名单", "filter_table": "筛选表格", "forwarding_hosts": "转发主机", "forwarding_hosts_add_hint": "你可以指定 IPv4/IPv6 地址、CIDR 表示的网络、主机名 (解析为 IP 地址),或者邮箱域名 (查询 SPF 记录或 MX 记录并解析为 IP 地址)。", @@ -295,8 +298,8 @@ "rspamd_com_settings": "设置名称将会自动生成,请看参考下方的示例预设。查看Rspamd 文档以了解更多的细节", "rspamd_global_filters": "全局过滤规则", "rspamd_global_filters_agree": "我会小心谨慎的!", - "rspamd_global_filters_info": "全局过滤规则包含了不同类型的全局黑名单和白名单。", - "rspamd_global_filters_regex": "它们的名字解释了它们的用途。所有内容必须包含 \"/pattern/options\" 格式的合法表达式 (例如 /.+@domain\\.tld/i)。
    \r\n 因为仅对正则表达式执行了基本的检查,Rspamd 的功能仍可能因正则表达式语法问题出现错误。
    \r\n Rspamd 会在规则更改后读取其内容。 如果你遇到了问题,重启 Rspamd 以强制重载规则。
    黑名单中的项目会被系统排除。", + "rspamd_global_filters_info": "全局过滤规则包含了不同类型的全局阻止名单和允许名单。", + "rspamd_global_filters_regex": "它们的名字解释了它们的用途。所有内容必须包含 \"/pattern/options\" 格式的合法表达式 (例如 /.+@domain\\.tld/i)。
    \n 因为仅对正则表达式执行了基本的检查,Rspamd 的功能仍可能因正则表达式语法问题出现错误。
    \n Rspamd 会在规则更改后读取其内容。 如果你遇到了问题,重启 Rspamd 以强制重载规则。
    阻止名单中的项目会被系统排除。", "rspamd_settings_map": "Rspamd 设置", "sal_level": "Moo 等级", "save": "保存更改", @@ -358,7 +361,56 @@ "ip_check_disabled": "IP 检查已禁用。你可透过以下路径启用
    系统 > 配置 > 选项 > 页面自定义", "queue_unban": "解除封禁", "allowed_methods": "访问控制允许方式", - "allowed_origins": "访问控制允许原" + "allowed_origins": "访问控制允许原", + "iam": "身份识别提供者", + "iam_attribute_field": "Attribute 域", + "iam_authorize_url": "Authorization endpoint", + "iam_auth_flow": "认证流程", + "iam_basedn": "Base DN", + "iam_client_id": "客户端 ID", + "iam_client_secret": "客户端凭据", + "iam_client_scopes": "客户端 Scopes", + "iam_default_template": "默认模板", + "iam_default_template_description": "如果未为用户分配模板,则在创建邮箱时将使用默认模板,但在更新邮箱时不会使用默认模板。", + "iam_description": "配置外部认证提供者
    如果已设置好属性映射,用户在首次登录时将会自动创建其 Mailbox。", + "iam_host": "Host", + "iam_host_info": "请输入一个或多个 LDAP 主机,使用英文逗号分隔。", + "iam_import_users": "导入用户", + "iam_mapping": "属性映射", + "iam_bindpass": "密码绑定(Bind Password)", + "iam_periodic_full_sync": "周期性全量同步", + "iam_port": "端口", + "iam_realm": "Realm", + "iam_redirect_url": "重定向 Url", + "iam_rest_flow": "Mailpassword 流程", + "iam_server_url": "服务器 Url", + "iam_sso": "单点登录(SSO)", + "iam_sync_interval": "同步/导入周期(min)", + "iam_test_connection": "测试连接", + "iam_token_url": "Token endpoint", + "iam_userinfo_url": "User info endpoint", + "iam_username_field": "Username 域", + "iam_binddn": "Bind DN", + "iam_use_ssl": "使用 SSL", + "iam_use_tls": "使用 StartTLS", + "iam_version": "版本", + "ignore_ssl_error": "忽略 SSL 错误", + "iam_auth_flow_info": "除了在单点登录(SSO)中使用的 Authorization Code 流程(在 Keycloak 中是标准流程)之外,mailcow 还支持使用 Credentials 的身份认证流程。Mailpassword 流程尝试通过 Keycloak 的 Admin REST API 验证用户凭据,mailcow 会从 Keycloak 中的 mailcow_password 属性中获取哈希后的密码。", + "filter": "过滤", + "iam_extra_permission": "要使以下设置生效,Keycloak 中的 mailcow 客户端需要一个 服务账户(Service account) 以及 查看用户(view-users) 的权限。", + "domainadmin_quicklink": "隐藏指向域管理员登陆页面的快捷链接", + "force_sso_text": "如果配置了外部的 OIDC 认证,这个选项隐藏默认的 mailcow 登陆界面,只显示单点登录(SSO)的按钮", + "iam_login_provisioning": "登录时自动创建用户", + "login_page": "登陆页面", + "iam_use_ssl_info": "如果使用了 SSL,且端口被设置为 389,该端口将自动被覆盖为 636。", + "quicklink_text": "显示或隐藏登陆表单下面指向其他登陆页面的快捷链接", + "user_quicklink": "隐藏指向用户登陆页面的快捷链接", + "admin_quicklink": "隐藏指向管理员登陆页面的快捷链接", + "force_sso": "强制要求单点登录(SSO)", + "user_link": "自定义链接", + "app_hide": "在登入界面隐藏", + "needs_restart": "需要重启", + "iam_use_tls_info": "如果使用了 TLS,必须使用 LDAP 服务器的默认端口(389)。SSL 的端口不能使用。" }, "danger": { "access_denied": "访问被拒绝或者表单数据无效", @@ -495,7 +547,15 @@ "webauthn_authenticator_failed": "找不到所选的 authenticator", "webauthn_publickey_failed": "没有为选定的身份验证器保存公钥", "webauthn_username_failed": "所选的 authenticator 属于另一个账户", - "demo_mode_enabled": "演示模式已开启" + "demo_mode_enabled": "演示模式已开启", + "generic_server_error": "服务器错误。请联系您的管理员。", + "authsource_in_use": "由于当前有一个或多个用户正在使用该身份提供者(IDP),因此无法更改或删除。", + "iam_test_connection": "连接失败", + "required_data_missing": "缺少需要的 %s 数据", + "max_age_invalid": "最大有效时间 %s 无效", + "mode_invalid": "模式 %s 无效", + "mx_invalid": "MX 记录 %s 无效", + "version_invalid": "版本 %s 无效" }, "debug": { "chart_this_server": "图表 (此服务器)", @@ -640,7 +700,7 @@ "sieve_desc": "简短描述", "sieve_type": "过滤器类型", "skipcrossduplicates": "跳过其他文件夹中已存在的邮件(保留已经存在的邮件)", - "sogo_access": "直接转发给 SOGo", + "sogo_access": "直接转到 SOGo", "sogo_access_info": "登录后,用户会自动跳转到 SOGo。", "sogo_visible": "SOGo 显示的别名", "sogo_visible_info": "此设置只影响 SOGo 上可显示的对象 (指向本地邮箱的共享或非共享别名地址)。如果设置为隐藏,则别名地址不会作为可选发件人的下拉项显示。", @@ -681,7 +741,20 @@ "domain_footer_skip_replies": "在回信中忽略 footer", "footer_exclude": "从 footer 中排除", "last_modified": "上次修改时间", - "pushover_sound": "声音" + "pushover_sound": "声音", + "internal": "内部的", + "internal_info": "内部的别名只能在域内部或者别名域内部访问。", + "mta_sts": "邮件传输代理严格传输安全协议(MTA-STS)", + "mta_sts_version": "版本", + "mta_sts_info": "MTA-STS 是一项 MTA 标准,它强制要求 MTA 间传输必须使用 TLS 和有效的证书。
    当因没有 DNSSEC 而无法使用 DANE 时,该标准将被启用。
    注意:若收件域支持基于 DNSSEC 的 DANE 协议,则系统将始终优先采用 DANE —— MTA-STS 仅作为备用机制存在。", + "mta_sts_version_info": "定义 MTA-STS 标准的版本——当前仅 STSv1 为有效版本。", + "mta_sts_mode": "模式", + "mta_sts_mode_info": "提供三种可选模式:
    • 测试模式——仅监控策略执行情况,违反策略不会产生实际影响。
    • 强制模式——严格执行策略,拒绝所有未使用有效 TLS 加密的连接。
    • 禁用模式——发布策略但不生效。
    ", + "mta_sts_max_age": "最长有效期", + "mta_sts_max_age_info": "接收方邮件服务器可缓存该策略的时长(秒),超出后需重新获取策略。", + "mta_sts_mx": "MX 服务器", + "mta_sts_mx_info": "仅允许向明确列出的邮件服务器发送邮件;发送方 MTA 会验证 DNS MX 记录的主机名是否与策略列表匹配,并仅允许携带有效 TLS 证书的投递(可防范中间人攻击)。", + "mta_sts_mx_notice": "可配置多个 MX 服务器(以逗号分隔)。" }, "fido2": { "confirm": "确认", @@ -744,7 +817,15 @@ "new_password_confirm": "确认新密码", "reset_password": "重置密码", "request_reset_password": "请求重置密码", - "invalid_pass_reset_token": "密码重置 token 无效或已过期。
    请重新获取新的密码重置链接。" + "invalid_pass_reset_token": "密码重置 token 无效或已过期。
    请重新获取新的密码重置链接。", + "login_user": "用户登录", + "login_dadmin": "域管理员登录", + "login_admin": "管理员登录", + "login_linkstext": "不是正确的登陆页面?", + "login_usertext": "以用户身份登陆", + "login_domainadmintext": "以域管理员身份登陆", + "login_admintext": "以管理员身份登陆", + "email": "邮箱地址" }, "mailbox": { "action": "操作", @@ -848,9 +929,9 @@ "recipient_map": "收件人映射", "recipient_map_info": "收件人映射用于在邮件被发送前替换收件人的地址。", "recipient_map_new": "新收件人", - "recipient_map_new_info": "新收件人必须为合法的邮箱地址。", + "recipient_map_new_info": "收件人映射的目标必须为合法的邮件地址或域名。", "recipient_map_old": "原收件人", - "recipient_map_old_info": "原收件人必须为合法的邮箱地址。", + "recipient_map_old_info": "原收件人必须为合法的邮箱地址或域名。", "recipient_maps": "收件人映射", "relay_all": "中继所有收件人", "remove": "删除", @@ -919,7 +1000,9 @@ "max_quota": "每个信箱的最大容量配额", "relay_unknown": "转发未知信箱", "templates": "模板", - "template": "模板" + "template": "模板", + "iam": "身份提供者(IDP)", + "internal": "内部的" }, "oauth2": { "access_denied": "请作为邮箱所有者登录以使用 OAuth2 授权。", @@ -950,8 +1033,8 @@ "neutral_danger": "无危险等级", "notified": "已发送通知", "qhandler_success": "已成功向系统发送请求,现在你可以关闭这个窗口了。", - "qid": "Rspamd QID", - "qinfo": "隔离系统会把已被拒绝接收的邮件以及作为拷贝发送到垃圾箱的邮件保存到数据库中 (发件人会知道)。\r\n
    \"学习为垃圾并删除\" 会根据贝叶斯定理将消息作为垃圾学习并计算其模糊特征以拒绝未来收到相似消息。\r\n
    请注意,这取决于你的系统资源,学习多个消息可能会花费较长时间。
    黑名单中项目会被隔离系统排除。", + "qid": "Rspamd 队列ID(QID)", + "qinfo": "隔离系统会把已被拒绝接收的邮件以及作为拷贝发送到垃圾箱的邮件保存到数据库中 (发件人会知道)。\n
    \"学习为垃圾并删除\" 会根据贝叶斯定理将消息作为垃圾学习并计算其模糊特征以拒绝未来收到相似消息。\n
    请注意,这取决于你的系统资源,学习多个消息可能会花费较长时间。
    阻止名单中项目会被隔离系统排除。", "qitem": "隔离项目", "quarantine": "隔离", "quick_actions": "操作", @@ -1097,7 +1180,9 @@ "recovery_email_sent": "重置邮件已发送至 %s", "template_added": "新增了模板 %s", "template_modified": "模板 %s 的修改已保存", - "template_removed": "模板 ID %s 已删除" + "template_removed": "模板 ID %s 已删除", + "iam_test_connection": "连接成功", + "custom_login_modified": "登陆选项保存成功" }, "tfa": { "api_register": "%s 使用了 Yubico Cloud API,请在此为你的密钥获取 API 密钥", @@ -1240,8 +1325,8 @@ "spam_score_reset": "重置为服务器默认值", "spamfilter": "垃圾邮件过滤器", "spamfilter_behavior": "分数", - "spamfilter_bl": "黑名单", - "spamfilter_bl_desc": "黑名单中地址总是会被标记为垃圾邮件。被拒绝的邮件不会进入隔离区。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。", + "spamfilter_bl": "阻止名单", + "spamfilter_bl_desc": "阻止名单中地址总是会被标记为垃圾邮件。被拒绝的邮件不会进入隔离区。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。", "spamfilter_default_score": "默认值", "spamfilter_green": "绿色: 此消息不是垃圾邮件", "spamfilter_hint": "第一个值表示\"低垃圾邮件分数\",第二个值表示\"高垃圾邮件分数\"。", @@ -1252,8 +1337,8 @@ "spamfilter_table_empty": "数据为空", "spamfilter_table_remove": "删除", "spamfilter_table_rule": "规则", - "spamfilter_wl": "白名单", - "spamfilter_wl_desc": "白名单中地址永远不会被标记为垃圾邮件。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。", + "spamfilter_wl": "允许名单", + "spamfilter_wl_desc": "允许名单中地址永远不会被标记为垃圾邮件。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。", "spamfilter_yellow": "黄色: 此为垃圾邮件,会被标记为垃圾邮件并且移入垃圾邮件文件夹", "status": "状态", "sync_jobs": "同步任务", @@ -1292,7 +1377,11 @@ "password_reset_info": "如果不提供密码重置邮箱,此功能将无法使用。", "pushover_sound": "声音", "value": "值", - "attribute": "属性" + "attribute": "属性", + "protocols": "协议", + "authentication": "认证", + "tfa_info": "两步验证有助于保护您的账户安全。启用后,对于不支持两步验证的应用程序或服务(例如邮件客户端),需要使用应用专用密码进行登录。", + "overview": "概览" }, "warning": { "cannot_delete_self": "不能删除已登录的用户", @@ -1332,4 +1421,4 @@ "loadingRecords": "加载中...", "zeroRecords": "未找到符合条件的记录" } -} \ No newline at end of file +} diff --git a/mailcow/data/web/lang/lang.zh-tw.json b/mailcow/data/web/lang/lang.zh-tw.json index aaa6d75..592efa8 100644 --- a/mailcow/data/web/lang/lang.zh-tw.json +++ b/mailcow/data/web/lang/lang.zh-tw.json @@ -1324,4 +1324,4 @@ "hold_mail": "保留", "unhold_mail": "取消保留" } -} \ No newline at end of file +} diff --git a/mailcow/data/web/mta-sts.php b/mailcow/data/web/mta-sts.php new file mode 100644 index 0000000..650b8b5 --- /dev/null +++ b/mailcow/data/web/mta-sts.php @@ -0,0 +1,31 @@ + diff --git a/mailcow/data/web/sogo-auth.php b/mailcow/data/web/sogo-auth.php index 5e0f3c3..962627b 100644 --- a/mailcow/data/web/sogo-auth.php +++ b/mailcow/data/web/sogo-auth.php @@ -64,7 +64,7 @@ elseif (isset($_GET['login'])) { ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) )); // redirect to sogo (sogo will get the correct credentials via nginx auth_request - header("Location: /SOGo/so/{$login}"); + header("Location: /SOGo/so/"); exit; } } @@ -81,10 +81,7 @@ elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HT } require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php'; - // extract email address from "/SOGo/so/user@domain/xy" - $url_parts = explode("/", $_SERVER['HTTP_X_ORIGINAL_URI']); $email_list = array( - $url_parts[3], // Requested mailbox ($_SESSION['mailcow_cc_username'] ?? ''), // Current user ($_SESSION["dual-login"]["username"] ?? ''), // Dual login user ); @@ -94,7 +91,8 @@ elseif (isset($_SERVER['HTTP_X_ORIGINAL_URI']) && strcasecmp(substr($_SERVER['HT !empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL) && is_array($_SESSION[$session_var_user_allowed]) && - in_array($email, $_SESSION[$session_var_user_allowed]) + in_array($email, $_SESSION[$session_var_user_allowed]) && + !$_SESSION['pending_pw_update'] ) { $username = $email; $password = file_get_contents("/etc/sogo-sso/sogo-sso.pass"); diff --git a/mailcow/data/web/templates/admin/tab-config-customize.twig b/mailcow/data/web/templates/admin/tab-config-customize.twig index c2071c3..aecf184 100644 --- a/mailcow/data/web/templates/admin/tab-config-customize.twig +++ b/mailcow/data/web/templates/admin/tab-config-customize.twig @@ -51,7 +51,41 @@

    - {{ lang.admin.app_links }}
    + {{ lang.admin.login_page }}
    +
    +
    +

    {{ lang.admin.quicklink_text }}

    +
    + + +
    +
    + + +
    +
    + + +
    +

    {{ lang.admin.force_sso_text|raw }}

    +
    + + +
    +

    + +

    +
    +
    + {{ lang.admin.app_links }}

    {{ lang.admin.merged_vars_hint|raw }}

    diff --git a/mailcow/data/web/templates/admin/tab-config-f2b.twig b/mailcow/data/web/templates/admin/tab-config-f2b.twig index 75c6266..29535b7 100644 --- a/mailcow/data/web/templates/admin/tab-config-f2b.twig +++ b/mailcow/data/web/templates/admin/tab-config-f2b.twig @@ -110,7 +110,7 @@

    - + {{ active_ban.network }} ({{ active_ban.banned_until }}) @@ -118,8 +118,8 @@ - {% if active_ban.queued_for_unban == 0 %} [{{ lang.admin.queue_unban }}] - [whitelist] - [blacklist (needs restart)] + [allowlist] + [denylist ({{ lang.admin.needs_restart }})] {% else %} {{ lang.admin.unban_pending }} {% endif %} @@ -130,7 +130,7 @@

    - + {{ perm_ban.network }} diff --git a/mailcow/data/web/templates/admin/tab-config-identity-provider.twig b/mailcow/data/web/templates/admin/tab-config-identity-provider.twig index dce26f8..4572d7f 100644 --- a/mailcow/data/web/templates/admin/tab-config-identity-provider.twig +++ b/mailcow/data/web/templates/admin/tab-config-identity-provider.twig @@ -64,10 +64,42 @@

    - +
    - +
    + + + +
    + +
    +
    +
    +
    +
    + + {% for key, url in iam_settings.redirect_url_extra %} +
    +
    +
    + +
    +
    + +
    +
    +
    + {% endfor %} +
    +
    +
    + +
    +
    + +
    +
    @@ -187,6 +219,16 @@
    +
    +
    + +
    +
    +
    + +
    +
    +
    @@ -274,10 +316,42 @@
    - +
    - +
    + + + +
    + +
    +
    +
    +
    +
    + + {% for key, url in iam_settings.redirect_url_extra %} +
    +
    +
    + +
    +
    + +
    +
    +
    + {% endfor %} +
    +
    +
    + +
    +
    + +
    +
    @@ -366,7 +440,7 @@
    -
    +
    @@ -376,6 +450,16 @@
    +
    +
    + +
    +
    +
    + +
    +
    +
    @@ -582,6 +666,16 @@
    +
    +
    + +
    +
    +
    + +
    +
    +
    diff --git a/mailcow/data/web/templates/admin_index.twig b/mailcow/data/web/templates/admin_index.twig index 6f22388..0dcb914 100644 --- a/mailcow/data/web/templates/admin_index.twig +++ b/mailcow/data/web/templates/admin_index.twig @@ -5,13 +5,28 @@ {% block content %}
    +
    -
    +
    {{ lang.login.login_admin }}
    +
    + + +
    -
    - - -
    +
    -
    {{ lang.login.other_logins }}
    +
    {{ lang.login.other_logins }}
    @@ -86,6 +88,15 @@ {% endif %}
    + + {% if custom_login.hide_user_quicklink != 1 or custom_login.hide_domainadmin_quicklink != 1 %} +

    + {{ lang.login.login_linkstext }}
    + {% if custom_login.hide_user_quicklink != 1 %}{{ lang.login.login_usertext }}{% endif %} + {% if custom_login.hide_user_quicklink != 1 and custom_login.hide_domainadmin_quicklink != 1 %}|{% endif %} + {% if custom_login.hide_domainadmin_quicklink != 1 %}{{ lang.login.login_domainadmintext }}{% endif %} +

    + {% endif %}
    {% endblock %} diff --git a/mailcow/data/web/templates/base.twig b/mailcow/data/web/templates/base.twig index 642d130..98fdd86 100644 --- a/mailcow/data/web/templates/base.twig +++ b/mailcow/data/web/templates/base.twig @@ -144,7 +144,7 @@
    -{% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active and not is_root_uri %} +{% if ui_texts.ui_announcement_text and ui_texts.ui_announcement_active and not is_root_uri and mailcow_cc_username %}
    {{ ui_texts.ui_announcement_text }}
    @@ -166,6 +166,7 @@ var lang_danger = {{ lang_danger|raw }}; var docker_timeout = {{ docker_timeout|raw }} * 1000; var mailcow_cc_role = '{{ mailcow_cc_role }}'; + {% if mailcow_cc_username %} var mailcow_info = { version_tag: '{{ mailcow_info.version_tag }}', last_version_tag: '{{ mailcow_info.last_version_tag }}', @@ -175,6 +176,17 @@ project_repo: '{{ mailcow_info.git_repo }}', branch: '{{ mailcow_info.mailcow_branch }}' }; + {% else %} + var mailcow_info = { + version_tag: '', + last_version_tag: '', + updatedAt: '', + project_url: '', + project_owner: '', + project_repo: '', + branch: '' + }; + {% endif %} $(window).scroll(function() { sessionStorage.scrollTop = $(this).scrollTop(); diff --git a/mailcow/data/web/templates/domainadmin_index.twig b/mailcow/data/web/templates/domainadmin_index.twig index 003fdba..2fc7bad 100644 --- a/mailcow/data/web/templates/domainadmin_index.twig +++ b/mailcow/data/web/templates/domainadmin_index.twig @@ -5,13 +5,28 @@ {% block content %}
    +
    -
    +
    {{ lang.login.login_dadmin }}
    +
    + + +
    -
    - - -
    +
    -
    {{ lang.login.other_logins }}
    +
    {{ lang.login.other_logins }}
    @@ -86,6 +88,15 @@ {% endif %}
    + + {% if custom_login.hide_user_quicklink != 1 or custom_login.hide_admin_quicklink != 1 %} +

    + {{ lang.login.login_linkstext }}
    + {% if custom_login.hide_user_quicklink != 1 %}{{ lang.login.login_usertext }}{% endif %} + {% if custom_login.hide_user_quicklink != 1 and custom_login.hide_admin_quicklink != 1 %}|{% endif %} + {% if custom_login.hide_admin_quicklink != 1 %}{{ lang.login.login_admintext }}{% endif %} +

    + {% endif %}
    {% endblock %} diff --git a/mailcow/data/web/templates/edit/alias.twig b/mailcow/data/web/templates/edit/alias.twig index 48d1961..64a6e70 100644 --- a/mailcow/data/web/templates/edit/alias.twig +++ b/mailcow/data/web/templates/edit/alias.twig @@ -6,6 +6,7 @@
    + {% if not skip_sogo %} {% endif %} @@ -33,8 +34,12 @@
    -

    {{ lang.edit.sogo_visible_info }}

    + {{ lang.edit.sogo_visible_info }} {% endif %} +
    + +
    + {{ lang.edit.internal_info }}

    diff --git a/mailcow/data/web/templates/edit/app-passwd.twig b/mailcow/data/web/templates/edit/app-passwd.twig index 46dc648..efab887 100644 --- a/mailcow/data/web/templates/edit/app-passwd.twig +++ b/mailcow/data/web/templates/edit/app-passwd.twig @@ -13,15 +13,15 @@
    - +
    - +
    - +
    - +
    diff --git a/mailcow/data/web/templates/edit/domain.twig b/mailcow/data/web/templates/edit/domain.twig index 6d1d85c..8335307 100644 --- a/mailcow/data/web/templates/edit/domain.twig +++ b/mailcow/data/web/templates/edit/domain.twig @@ -8,6 +8,7 @@ +
    @@ -38,7 +39,7 @@
    - +
    @@ -57,6 +58,7 @@
    + {% if acl.domain_relayhost == '1' %}
    @@ -75,6 +77,7 @@
    + {% endif %} {% if mailcow_cc_role == 'admin' %}
    @@ -146,7 +149,7 @@
    {{ lang.edit.created_on }}: {{ result.created }} - {{ lang.edit.last_modified }}: {{ result.modified }} + {{ lang.edit.last_modified }}: {{ result.modified|default(lang.user.never) }}
    @@ -278,6 +281,82 @@
    +
    +
    +
    + +
    +
    +

    {{ lang.edit.mta_sts }}

    +

    {{ lang.edit.mta_sts_info|raw }}

    +
    + + +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + {{ lang.edit.mta_sts_mx_notice|raw }} +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + {% if mta_sts == false %} + + {% else %} + + {% endif %} +
    +
    + +
    +
    +
    diff --git a/mailcow/data/web/templates/edit/mailbox-templates.twig b/mailcow/data/web/templates/edit/mailbox-templates.twig index 6d150b2..65a83cd 100644 --- a/mailcow/data/web/templates/edit/mailbox-templates.twig +++ b/mailcow/data/web/templates/edit/mailbox-templates.twig @@ -36,6 +36,23 @@ 0 = ∞
    +
    + +
    +
    + + + + + + + + +
    +

    {{ lang.user.tag_help_explain|raw }}

    +

    {{ lang.user.tag_help_example|raw }}

    +
    +
    diff --git a/mailcow/data/web/templates/edit/mailbox.twig b/mailcow/data/web/templates/edit/mailbox.twig index 32a3706..a223a75 100644 --- a/mailcow/data/web/templates/edit/mailbox.twig +++ b/mailcow/data/web/templates/edit/mailbox.twig @@ -114,10 +114,11 @@ {{ lang.edit.sender_acl_info|raw }}
    + {% if acl.mailbox_relayhost == '1' %}
    - {% for rlyhost in rlyhosts %}
    +
    + {% endif %} +
    + +
    +
    + + + +
    +

    {{ lang.user.tag_help_explain|raw }}

    +

    {{ lang.user.tag_help_example|raw }}

    @@ -298,7 +327,7 @@
    {{ lang.edit.created_on }}: {{ result.created }} - {{ lang.edit.last_modified }}: {{ result.modified }} + {{ lang.edit.last_modified }}: {{ result.modified|default(lang.user.never) }}
    diff --git a/mailcow/data/web/templates/modals/mailbox.twig b/mailcow/data/web/templates/modals/mailbox.twig index 85091da..1e8ee53 100644 --- a/mailcow/data/web/templates/modals/mailbox.twig +++ b/mailcow/data/web/templates/modals/mailbox.twig @@ -76,6 +76,23 @@
    +
    + +
    +
    + + + + + + + + +
    +

    {{ lang.user.tag_help_explain|raw }}

    +

    {{ lang.user.tag_help_example|raw }}

    +
    +
    @@ -246,6 +263,23 @@ 0 = ∞
    +
    + +
    +
    + + + + + + + + +
    +

    {{ lang.user.tag_help_explain|raw }}

    +

    {{ lang.user.tag_help_example|raw }}

    +
    +
    @@ -743,6 +777,7 @@
    @@ -18,12 +19,13 @@ {{ lang.mailbox.toggle_all }} {{ lang.mailbox.quick_actions }} diff --git a/mailcow/data/web/templates/user_index.twig b/mailcow/data/web/templates/user_index.twig index 4ed9421..28eb638 100644 --- a/mailcow/data/web/templates/user_index.twig +++ b/mailcow/data/web/templates/user_index.twig @@ -5,13 +5,30 @@ {% block content %}
    +
    -
    +
    {{ lang.login.login_user }}
    + {% if not oauth2_request %} +
    + + +
    + {% endif %}
    +
    - - {% if not oauth2_request %} -
    - - -
    - {% endif %} +
    - -
    {{ lang.login.other_logins }}
    +
    {{ lang.login.other_logins }}
    + {% endif %}
    {% if has_iam_sso %} {{ lang.admin.iam_sso }} {% endif %} + {% if custom_login.force_sso != 1 %} {{ lang.login.fido2_webauthn }} + {% endif %}
    {% if login_delay %}

    {{ lang.login.delayed|format(login_delay) }}

    @@ -96,9 +101,20 @@ {% endfor %} {% endfor %}
    +
    +
    {% endif %}
    + + {% if custom_login.hide_admin_quicklink != 1 or custom_login.hide_domainadmin_quicklink != 1 %} +

    + {{ lang.login.login_linkstext }}
    + {% if custom_login.hide_admin_quicklink != 1 %}{{ lang.login.login_admintext }}{% endif %} + {% if custom_login.hide_admin_quicklink != 1 and custom_login.hide_domainadmin_quicklink != 1 %}|{% endif %} + {% if custom_login.hide_domainadmin_quicklink != 1 %}{{ lang.login.login_domainadmintext }}{% endif %} +

    + {% endif %}
    {% if not oauth2_request and ui_texts.help_text %} diff --git a/mailcow/docker-compose.yml b/mailcow/docker-compose.yml index 0cd4491..ae9a18d 100644 --- a/mailcow/docker-compose.yml +++ b/mailcow/docker-compose.yml @@ -24,7 +24,7 @@ services: stop_grace_period: 45s volumes: - mysql-vol-1:/var/lib/mysql/ - - mysql-socket-vol-1:/var/run/mysqld/ + - mysql-socket-vol-1:/var/run/mysqld/:z - ./data/conf/mysql/:/etc/mysql/conf.d/:ro,Z environment: - TZ=${TZ} @@ -42,7 +42,7 @@ services: - mysql redis-mailcow: - image: redis:7.4.2-alpine + image: redis:7.4.6-alpine entrypoint: ["/bin/sh","/redis-conf.sh"] volumes: - redis-vol-1:/data/ @@ -65,7 +65,7 @@ services: - redis clamd-mailcow: - image: ghcr.io/mailcow/clamd:1.70 + image: ghcr.io/mailcow/clamd:1.71 restart: always depends_on: unbound-mailcow: @@ -84,7 +84,7 @@ services: - clamd rspamd-mailcow: - image: ghcr.io/mailcow/rspamd:2.1 + image: ghcr.io/mailcow/rspamd:2.4 stop_grace_period: 30s depends_on: - dovecot-mailcow @@ -117,7 +117,7 @@ services: - rspamd php-fpm-mailcow: - image: ghcr.io/mailcow/phpfpm:1.93 + image: ghcr.io/mailcow/phpfpm:8.2.29 command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" depends_on: - redis-mailcow @@ -134,7 +134,7 @@ services: - ./data/web/inc/functions.ratelimit.inc.php:/mailcowauth/functions.ratelimit.inc.php:z - ./data/web/inc/functions.acl.inc.php:/mailcowauth/functions.acl.inc.php:z - rspamd-vol-1:/var/lib/rspamd - - mysql-socket-vol-1:/var/run/mysqld/ + - mysql-socket-vol-1:/var/run/mysqld/:z - ./data/conf/sogo/:/etc/sogo/:z - ./data/conf/rspamd/meta_exporter:/meta_exporter:ro,z - ./data/conf/phpfpm/crons:/crons:z @@ -176,6 +176,7 @@ services: - COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_NAME:-mailcow-dockerized} - SKIP_FTS=${SKIP_FTS:-y} - SKIP_CLAMD=${SKIP_CLAMD:-n} + - SKIP_OLEFY=${SKIP_OLEFY:-n} - SKIP_SOGO=${SKIP_SOGO:-n} - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} - MASTER=${MASTER:-y} @@ -187,10 +188,10 @@ services: restart: always labels: ofelia.enabled: "true" - ofelia.job-exec.phpfpm_keycloak_sync.schedule: "@every 1m" + ofelia.job-exec.phpfpm_keycloak_sync.schedule: "0 * * * * *" ofelia.job-exec.phpfpm_keycloak_sync.no-overlap: "true" ofelia.job-exec.phpfpm_keycloak_sync.command: "/bin/bash -c \"php /crons/keycloak-sync.php || exit 0\"" - ofelia.job-exec.phpfpm_ldap_sync.schedule: "@every 1m" + ofelia.job-exec.phpfpm_ldap_sync.schedule: "0 * * * * *" ofelia.job-exec.phpfpm_ldap_sync.no-overlap: "true" ofelia.job-exec.phpfpm_ldap_sync.command: "/bin/bash -c \"php /crons/ldap-sync.php || exit 0\"" networks: @@ -199,7 +200,7 @@ services: - phpfpm sogo-mailcow: - image: ghcr.io/mailcow/sogo:1.133 + image: ghcr.io/mailcow/sogo:5.12.4 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} @@ -212,6 +213,7 @@ services: - ALLOW_ADMIN_EMAIL_LOGIN=${ALLOW_ADMIN_EMAIL_LOGIN:-n} - IPV4_NETWORK=${IPV4_NETWORK:-172.22.1} - SOGO_EXPIRE_SESSION=${SOGO_EXPIRE_SESSION:-480} + - SOGO_URL_ENCRYPTION_KEY=${SOGO_URL_ENCRYPTION_KEY:-SOGoSuperSecret0} - SKIP_SOGO=${SKIP_SOGO:-n} - MASTER=${MASTER:-y} - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} @@ -229,18 +231,18 @@ services: - ./data/conf/sogo/custom-fulllogo.png:/usr/lib/GNUstep/SOGo/WebServerResources/img/sogo-logo.png:z - ./data/conf/sogo/custom-theme.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/theme.js:z - ./data/conf/sogo/custom-sogo.js:/usr/lib/GNUstep/SOGo/WebServerResources/js/custom-sogo.js:z - - mysql-socket-vol-1:/var/run/mysqld/ + - mysql-socket-vol-1:/var/run/mysqld/:z - sogo-web-vol-1:/sogo_web - sogo-userdata-backup-vol-1:/sogo_backup labels: ofelia.enabled: "true" - ofelia.job-exec.sogo_sessions.schedule: "@every 1m" + ofelia.job-exec.sogo_sessions.schedule: "0 * * * * *" ofelia.job-exec.sogo_sessions.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool -v expire-sessions $${SOGO_EXPIRE_SESSION} || exit 0\"" - ofelia.job-exec.sogo_ealarms.schedule: "@every 1m" + ofelia.job-exec.sogo_ealarms.schedule: "0 * * * * *" ofelia.job-exec.sogo_ealarms.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-ealarms-notify -p /etc/sogo/cron.creds || exit 0\"" - ofelia.job-exec.sogo_eautoreply.schedule: "@every 5m" - ofelia.job-exec.sogo_eautoreply.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/cron.creds || exit 0\"" - ofelia.job-exec.sogo_backup.schedule: "@every 24h" + ofelia.job-exec.sogo_eautoreply.schedule: "0 */5 * * * *" + ofelia.job-exec.sogo_eautoreply.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/sieve.creds || exit 0\"" + ofelia.job-exec.sogo_backup.schedule: "0 0 0 * * *" ofelia.job-exec.sogo_backup.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu sogo /usr/sbin/sogo-tool backup /sogo_backup ALL || exit 0\"" restart: always networks: @@ -250,7 +252,7 @@ services: - sogo dovecot-mailcow: - image: ghcr.io/mailcow/dovecot:2.33 + image: ghcr.io/mailcow/dovecot:2.3.21.1 depends_on: - mysql-mailcow - netfilter-mailcow @@ -271,7 +273,7 @@ services: - ./data/conf/rspamd/custom/:/etc/rspamd/custom:z - ./data/assets/templates:/templates:z - rspamd-vol-1:/var/lib/rspamd - - mysql-socket-vol-1:/var/run/mysqld/ + - mysql-socket-vol-1:/var/run/mysqld/:z environment: - DOVECOT_MASTER_USER=${DOVECOT_MASTER_USER:-} - DOVECOT_MASTER_PASS=${DOVECOT_MASTER_PASS:-} @@ -308,22 +310,22 @@ services: tty: true labels: ofelia.enabled: "true" - ofelia.job-exec.dovecot_imapsync_runner.schedule: "@every 1m" + ofelia.job-exec.dovecot_imapsync_runner.schedule: "0 * * * * *" ofelia.job-exec.dovecot_imapsync_runner.no-overlap: "true" ofelia.job-exec.dovecot_imapsync_runner.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu nobody /usr/local/bin/imapsync_runner.pl || exit 0\"" - ofelia.job-exec.dovecot_trim_logs.schedule: "@every 1m" + ofelia.job-exec.dovecot_trim_logs.schedule: "0 * * * * *" ofelia.job-exec.dovecot_trim_logs.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/trim_logs.sh || exit 0\"" - ofelia.job-exec.dovecot_quarantine.schedule: "@every 20m" + ofelia.job-exec.dovecot_quarantine.schedule: "0 */20 * * * *" ofelia.job-exec.dovecot_quarantine.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/quarantine_notify.py || exit 0\"" - ofelia.job-exec.dovecot_clean_q_aged.schedule: "@every 24h" + ofelia.job-exec.dovecot_clean_q_aged.schedule: "0 0 0 * * *" ofelia.job-exec.dovecot_clean_q_aged.command: "/bin/bash -c \"[[ $${MASTER} == y ]] && /usr/local/bin/gosu vmail /usr/local/bin/clean_q_aged.sh || exit 0\"" - ofelia.job-exec.dovecot_maildir_gc.schedule: "@every 30m" + ofelia.job-exec.dovecot_maildir_gc.schedule: "0 */30 * * * *" ofelia.job-exec.dovecot_maildir_gc.command: "/bin/bash -c \"source /source_env.sh ; /usr/local/bin/gosu vmail /usr/local/bin/maildir_gc.sh\"" ofelia.job-exec.dovecot_sarules.schedule: "@every 24h" ofelia.job-exec.dovecot_sarules.command: "/bin/bash -c \"/usr/local/bin/sa-rules.sh\"" - ofelia.job-exec.dovecot_fts.schedule: "@every 24h" + ofelia.job-exec.dovecot_fts.schedule: "0 0 0 * * *" ofelia.job-exec.dovecot_fts.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/optimize-fts.sh\"" - ofelia.job-exec.dovecot_repl_health.schedule: "@every 5m" + ofelia.job-exec.dovecot_repl_health.schedule: "0 */5 * * * *" ofelia.job-exec.dovecot_repl_health.command: "/bin/bash -c \"/usr/local/bin/gosu vmail /usr/local/bin/repl_health.sh\"" ulimits: nproc: 65535 @@ -337,12 +339,14 @@ services: - dovecot postfix-mailcow: - image: ghcr.io/mailcow/postfix:1.80 + image: ghcr.io/mailcow/postfix:3.7.11 depends_on: mysql-mailcow: condition: service_started unbound-mailcow: condition: service_healthy + postfix-tlspol-mailcow: + condition: service_started volumes: - ./data/hooks/postfix:/hooks:Z - ./data/conf/postfix:/opt/postfix/conf:z @@ -350,7 +354,7 @@ services: - postfix-vol-1:/var/spool/postfix - crypt-vol-1:/var/lib/zeyple - rspamd-vol-1:/var/lib/rspamd - - mysql-socket-vol-1:/var/run/mysqld/ + - mysql-socket-vol-1:/var/run/mysqld/:z environment: - LOG_LINES=${LOG_LINES:-9999} - TZ=${TZ} @@ -377,6 +381,28 @@ services: aliases: - postfix + postfix-tlspol-mailcow: + image: ghcr.io/mailcow/postfix-tlspol:1.8.22 + depends_on: + unbound-mailcow: + condition: service_healthy + volumes: + - postfix-tlspol-vol-1:/var/lib/postfix-tlspol + environment: + - LOG_LINES=${LOG_LINES:-9999} + - TZ=${TZ} + - REDIS_SLAVEOF_IP=${REDIS_SLAVEOF_IP:-} + - REDIS_SLAVEOF_PORT=${REDIS_SLAVEOF_PORT:-} + - REDISPASS=${REDISPASS} + - DEV_MODE=${DEV_MODE:-n} + restart: always + dns: + - ${IPV4_NETWORK:-172.22.1}.254 + networks: + mailcow-network: + aliases: + - postfix-tlspol + memcached-mailcow: image: memcached:alpine restart: always @@ -393,7 +419,7 @@ services: - php-fpm-mailcow - sogo-mailcow - rspamd-mailcow - image: ghcr.io/mailcow/nginx:1.03 + image: ghcr.io/mailcow/nginx:1.05 dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: @@ -404,7 +430,7 @@ services: - TZ=${TZ} - SKIP_SOGO=${SKIP_SOGO:-n} - SKIP_RSPAMD=${SKIP_RSPAMD:-n} - - DISABLE_IPv6=${DISABLE_IPv6:-n} + - ENABLE_IPV6=${ENABLE_IPV6:-true} - HTTP_REDIRECT=${HTTP_REDIRECT:-n} - PHPFPMHOST=${PHPFPMHOST:-} - SOGOHOST=${SOGOHOST:-} @@ -439,12 +465,11 @@ services: condition: service_started unbound-mailcow: condition: service_healthy - image: ghcr.io/mailcow/acme:1.92 + image: ghcr.io/mailcow/acme:1.94 dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: - LOG_LINES=${LOG_LINES:-9999} - - ACME_CONTACT=${ACME_CONTACT:-} - ADDITIONAL_SAN=${ADDITIONAL_SAN} - AUTODISCOVER_SAN=${AUTODISCOVER_SAN:-y} - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} @@ -469,7 +494,7 @@ services: - ./data/web/.well-known/acme-challenge:/var/www/acme:z - ./data/assets/ssl:/var/lib/acme/:z - ./data/assets/ssl-example:/var/lib/ssl-example/:ro,Z - - mysql-socket-vol-1:/var/run/mysqld/ + - mysql-socket-vol-1:/var/run/mysqld/:z restart: always networks: mailcow-network: @@ -477,7 +502,7 @@ services: - acme netfilter-mailcow: - image: ghcr.io/mailcow/netfilter:1.61 + image: ghcr.io/mailcow/netfilter:1.63 stop_grace_period: 30s restart: always privileged: true @@ -497,14 +522,14 @@ services: - /lib/modules:/lib/modules:ro watchdog-mailcow: - image: ghcr.io/mailcow/watchdog:2.07 + image: ghcr.io/mailcow/watchdog:2.09 dns: - ${IPV4_NETWORK:-172.22.1}.254 tmpfs: - /tmp volumes: - rspamd-vol-1:/var/lib/rspamd - - mysql-socket-vol-1:/var/run/mysqld/ + - mysql-socket-vol-1:/var/run/mysqld/:z - postfix-vol-1:/var/spool/postfix - ./data/assets/ssl:/etc/ssl/mail/:ro,z restart: always @@ -538,6 +563,7 @@ services: - IP_BY_DOCKER_API=${IP_BY_DOCKER_API:-0} - CHECK_UNBOUND=${CHECK_UNBOUND:-1} - SKIP_CLAMD=${SKIP_CLAMD:-n} + - SKIP_OLEFY=${SKIP_OLEFY:-n} - SKIP_LETS_ENCRYPT=${SKIP_LETS_ENCRYPT:-n} - SKIP_SOGO=${SKIP_SOGO:-n} - HTTPS_PORT=${HTTPS_PORT:-443} @@ -552,6 +578,7 @@ services: - MYSQL_REPLICATION_THRESHOLD=${MYSQL_REPLICATION_THRESHOLD:-1} - SOGO_THRESHOLD=${SOGO_THRESHOLD:-3} - POSTFIX_THRESHOLD=${POSTFIX_THRESHOLD:-8} + - POSTFIX_TLSPOL_THRESHOLD=${POSTFIX_TLSPOL_THRESHOLD:-8} - CLAMD_THRESHOLD=${CLAMD_THRESHOLD:-15} - DOVECOT_THRESHOLD=${DOVECOT_THRESHOLD:-12} - DOVECOT_REPL_THRESHOLD=${DOVECOT_REPL_THRESHOLD:-20} @@ -563,6 +590,7 @@ services: - OLEFY_THRESHOLD=${OLEFY_THRESHOLD:-5} - MAILQ_THRESHOLD=${MAILQ_THRESHOLD:-20} - MAILQ_CRIT=${MAILQ_CRIT:-30} + - DEV_MODE=${DEV_MODE:-n} networks: mailcow-network: aliases: @@ -589,7 +617,7 @@ services: - dockerapi olefy-mailcow: - image: ghcr.io/mailcow/olefy:1.14 + image: ghcr.io/mailcow/olefy:1.15 restart: always environment: - TZ=${TZ} @@ -601,6 +629,7 @@ services: - OLEFY_LOGLVL=20 - OLEFY_MINLENGTH=500 - OLEFY_DEL_TMP=1 + - SKIP_OLEFY=${SKIP_OLEFY:-n} networks: mailcow-network: aliases: @@ -627,41 +656,12 @@ services: aliases: - ofelia - ipv6nat-mailcow: - depends_on: - - unbound-mailcow - - mysql-mailcow - - redis-mailcow - - clamd-mailcow - - rspamd-mailcow - - php-fpm-mailcow - - sogo-mailcow - - dovecot-mailcow - - postfix-mailcow - - memcached-mailcow - - nginx-mailcow - - acme-mailcow - - netfilter-mailcow - - watchdog-mailcow - - dockerapi-mailcow - environment: - - TZ=${TZ} - image: robbertkl/ipv6nat - security_opt: - - label=disable - restart: always - privileged: true - network_mode: "host" - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - /lib/modules:/lib/modules:ro - networks: mailcow-network: driver: bridge driver_opts: com.docker.network.bridge.name: br-mailcow - enable_ipv6: true + enable_ipv6: ${ENABLE_IPV6:-true} ipam: driver: default config: @@ -676,6 +676,7 @@ volumes: redis-vol-1: rspamd-vol-1: postfix-vol-1: + postfix-tlspol-vol-1: crypt-vol-1: sogo-web-vol-1: sogo-userdata-backup-vol-1: diff --git a/mailcow/generate_config.sh b/mailcow/generate_config.sh index c7a7cb6..393d2fc 100755 --- a/mailcow/generate_config.sh +++ b/mailcow/generate_config.sh @@ -1,32 +1,34 @@ #!/usr/bin/env bash -set -o pipefail +# Ensure the script is run from the directory that contains a link of .env +# Resolve the directory this script lives in for consistent behavior when invoked from elsewhere +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" >/dev/null 2>&1 && pwd)" -if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then - echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; - echo "Please update to 5.x or use another distribution." +# Ensure script is executed in the mailcow installation directory by checking for a .env symlink that points to mailcow.conf +if [ ! -L "${PWD}/.env" ]; then + echo -e "\e[33mPlease run this script from the mailcow installation directory.\e[0m" + echo -e " \e[36mcd /path/to/mailcow && ./generate_config.sh\e[0m" exit 1 fi -if [[ "$(uname -r)" =~ ^4\.4\. ]]; then - if grep -q Ubuntu <<< "$(uname -a)"; then - echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; - echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\"" - exit 1 - fi +# Verify the .env symlink points to a mailcow.conf file +env_target="$(readlink -f "${PWD}/.env" 2>/dev/null || true)" +if [ -z "$env_target" ] || [ "$(basename "$env_target")" != "mailcow.conf" ]; then + echo -e "\e[31mThe found .env symlink does not point to a mailcow.conf file.\e[0m" + echo -e "\e[33mPlease create a symbolic link .env -> mailcow.conf inside the mailcow directory and run this script there.\e[0m" + echo -e "\e[33mNote: 'ln -s mailcow.conf .env' will create the symlink even if mailcow.conf does not yet exist.\e[0m" + echo -e " \e[36mcd /path/to/mailcow && ln -s mailcow.conf .env && ./generate_config.sh\e[0m" + exit 1 fi -if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\""; exit 1; fi -# This will also cover sort -if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\""; exit 1; fi -if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\""; exit 1; fi +# Load mailcow Generic Scripts +source _modules/scripts/core.sh +source _modules/scripts/ipv6_controller.sh -for bin in openssl curl docker git awk sha1sum grep cut; do - if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi -done +set -o pipefail -# Check Docker Version (need at least 24.X) -docker_version=$(docker version --format '{{.Server.Version}}' | cut -d '.' -f 1) +get_installed_tools +get_docker_version if [[ $docker_version -lt 24 ]]; then echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m" @@ -35,65 +37,7 @@ if [[ $docker_version -lt 24 ]]; then exit 1 fi -if docker compose > /dev/null 2>&1; then - if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then - COMPOSE_VERSION=native - echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m" - echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m" - sleep 2 - echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" - else - echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" - echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" - exit 1 - fi -elif docker-compose > /dev/null 2>&1; then - if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then - if docker-compose version --short | grep "^2." > /dev/null 2>&1; then - COMPOSE_VERSION=standalone - echo -e "\e[33mFound Docker Compose Standalone.\e[0m" - echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m" - sleep 2 - echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m" - else - echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" - echo -e "\e[31mPlease update/install manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" - exit 1 - fi - fi - -else - echo -e "\e[31mCannot find Docker Compose.\e[0m" - echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" - exit 1 -fi - -detect_bad_asn() { - echo -e "\e[33mDetecting if your IP is listed on Spamhaus Bad ASN List...\e[0m" - response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email") - if [ "$response" -eq 503 ]; then - if [ -z "$SPAMHAUS_DQS_KEY" ]; then - echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" - echo -e "\e[33mmailcow did not detected a value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf!\e[0m" - sleep 2 - echo "" - echo -e "\e[33mTo use the Spamhaus DNS Blocklists again, you will need to create a FREE account for their Data Query Service (DQS) at: https://www.spamhaus.com/free-trial/sign-up-for-a-free-data-query-service-account\e[0m" - echo -e "\e[33mOnce done, enter your DQS API key in mailcow.conf and mailcow will do the rest for you!\e[0m" - echo "" - sleep 2 - - else - echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" - echo -e "\e[32mmailcow detected a Value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf. Postfix will use DQS with the given API key...\e[0m" - fi - elif [ "$response" -eq 200 ]; then - echo -e "\e[33mCheck completed! Your IP is \e[32mclean\e[0m" - elif [ "$response" -eq 429 ]; then - echo -e "\e[33mCheck completed! \e[31mYour IP seems to be rate limited on the ASN Check service... please try again later!\e[0m" - else - echo -e "\e[31mCheck failed! \e[0mMaybe a DNS or Network problem?\e[0m" - fi -} +detect_bad_asn ### If generate_config.sh is started with --dev or -d it will not check out nightly or master branch and will keep on the current branch if [[ ${1} == "--dev" || ${1} == "-d" ]]; then @@ -217,6 +161,8 @@ if [ ! -z "${MAILCOW_BRANCH}" ]; then git_branch=${MAILCOW_BRANCH} fi +configure_ipv6 + [ ! -f ./data/conf/rspamd/override.d/worker-controller-password.inc ] && echo '# Placeholder' > ./data/conf/rspamd/override.d/worker-controller-password.inc cat << EOF > mailcow.conf @@ -226,7 +172,6 @@ cat << EOF > mailcow.conf # example.org is _not_ a valid hostname, use a fqdn here. # Default admin user is "admin" # Default password is "moohoo" - MAILCOW_HOSTNAME=${MAILCOW_HOSTNAME} # Password hash algorithm @@ -237,19 +182,16 @@ MAILCOW_PASS_SCHEME=BLF-CRYPT # ------------------------------ # SQL database configuration # ------------------------------ - DBNAME=mailcow DBUSER=mailcow # Please use long, random alphanumeric strings (A-Za-z0-9) - DBPASS=$(LC_ALL=C /dev/null | head -c 28) DBROOT=$(LC_ALL=C /dev/null | head -c 28) # ------------------------------ # REDIS configuration # ------------------------------ - REDISPASS=$(LC_ALL=C /dev/null | head -c 28) # ------------------------------ @@ -264,7 +206,6 @@ REDISPASS=$(LC_ALL=C /dev/null | head -c 28) # Example: HTTP_BIND=1.2.3.4 # For IPv4 leave it as it is: HTTP_BIND= & HTTPS_PORT= # For IPv6 see https://docs.mailcow.email/post_installation/firststeps-ip_bindings/ - HTTP_PORT=80 HTTP_BIND= @@ -272,14 +213,13 @@ HTTPS_PORT=443 HTTPS_BIND= # Redirect HTTP connections to HTTPS - y/n -HTTP_REDIRECT=n +HTTP_REDIRECT=y # ------------------------------ # Other bindings # ------------------------------ # You should leave that alone # Format: 11.22.33.44:25 or 12.34.56.78:465 etc. - SMTP_PORT=25 SMTPS_PORT=465 SUBMISSION_PORT=587 @@ -295,25 +235,22 @@ REDIS_PORT=127.0.0.1:7654 # Your timezone # See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones for a list of timezones # Use the column named 'TZ identifier' + pay attention for the column named 'Notes' - TZ=${MAILCOW_TZ} # Fixed project name # Please use lowercase letters only - COMPOSE_PROJECT_NAME=mailcowdockerized # Used Docker Compose version # Switch here between native (compose plugin) and standalone -# For more informations take a look at the mailcow docs regarding the configuration options. +# For more information take a look at the mailcow docs regarding the configuration options. # Normally this should be untouched but if you decided to use either of those you can switch it manually here. # Please be aware that at least one of those variants should be installed on your machine or mailcow will fail. - DOCKER_COMPOSE_VERSION=${COMPOSE_VERSION} # Set this to "allow" to enable the anyone pseudo user. Disabled by default. # When enabled, ACL can be created, that apply to "All authenticated users" -# This should probably only be activated on mail hosts, that are used exclusivly by one organisation. +# This should probably only be activated on mail hosts, that are used exclusively by one organisation. # Otherwise a user might share data with too many other users. ACL_ANYONE=disallow @@ -321,7 +258,6 @@ ACL_ANYONE=disallow # Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring # How long should objects remain in the garbage until they are being deleted? (value in minutes) # Check interval is hourly - MAILDIR_GC_TIME=7200 # Additional SAN for the certificate @@ -336,8 +272,6 @@ MAILDIR_GC_TIME=7200 #ADDITIONAL_SAN=srv1.example.net # ...or combine wildcard and static names: #ADDITIONAL_SAN=imap.*,srv1.example.com -# - ADDITIONAL_SAN= # Obtain certificates for autodiscover.* and autoconfig.* domains. @@ -354,61 +288,52 @@ AUTODISCOVER_SAN=y # If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root. # You can understand this as server_name directive in Nginx. # Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f - ADDITIONAL_SERVER_NAMES= # Skip running ACME (acme-mailcow, Let's Encrypt certs) - y/n - SKIP_LETS_ENCRYPT=n -# Create seperate certificates for all domains - y/n +# Create separate certificates for all domains - y/n # this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames # see https://doc.dovecot.org/admin_manual/ssl/sni_support ENABLE_SSL_SNI=n # Skip IPv4 check in ACME container - y/n - SKIP_IP_CHECK=n # Skip HTTP verification in ACME container - y/n - SKIP_HTTP_VERIFICATION=n # Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n - SKIP_UNBOUND_HEALTHCHECK=n # Skip ClamAV (clamd-mailcow) anti-virus (Rspamd will auto-detect a missing ClamAV container) - y/n - SKIP_CLAMD=${SKIP_CLAMD} -# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n +# Skip Olefy (olefy-mailcow) anti-virus for Office documents (Rspamd will auto-detect a missing Olefy container) - y/n +SKIP_OLEFY=n +# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n SKIP_SOGO=n # Skip FTS (Fulltext Search) for Dovecot on low-memory, low-threaded systems or if you simply want to disable it. # Dovecot inside mailcow use Flatcurve as FTS Backend. - SKIP_FTS=n # Dovecot Indexing (FTS) Process maximum heap size in MB, there is no recommendation, please see Dovecot docs. # Flatcurve (Xapian backend) is used as the FTS Indexer. It is supposed to be efficient in CPU and RAM consumption. # However: Please always monitor your Resource consumption! - FTS_HEAP=128 # Controls how many processes the Dovecot indexing process can spawn at max. # Too many indexing processes can use a lot of CPU and Disk I/O. -# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more informations - +# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more information FTS_PROCS=1 # Allow admins to log into SOGo as email user (without any password) - ALLOW_ADMIN_EMAIL_LOGIN=n # Enable watchdog (watchdog-mailcow) to restart unhealthy containers - USE_WATCHDOG=y # Send watchdog notifications by mail (sent from watchdog@MAILCOW_HOSTNAME) @@ -417,7 +342,6 @@ USE_WATCHDOG=y # 2. Mails are sent unsigned (no DKIM) # 3. If you use DMARC, create a separate DMARC policy ("v=DMARC1; p=none;" in _dmarc.MAILCOW_HOSTNAME) # Multiple rcpts allowed, NO quotation marks, NO spaces - #WATCHDOG_NOTIFY_EMAIL=a@example.com,b@example.com,c@example.com #WATCHDOG_NOTIFY_EMAIL= @@ -448,25 +372,20 @@ WATCHDOG_EXTERNAL_CHECKS=n WATCHDOG_VERBOSE=n # Max log lines per service to keep in Redis logs - LOG_LINES=9999 # Internal IPv4 /24 subnet, format n.n.n (expands to n.n.n.0/24) # Use private IPv4 addresses only, see https://en.wikipedia.org/wiki/Private_network#Private_IPv4_addresses - IPV4_NETWORK=172.22.1 # Internal IPv6 subnet in fc00::/7 # Use private IPv6 addresses only, see https://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses - IPV6_NETWORK=fd4d:6169:6c63:6f77::/64 # Use this IPv4 for outgoing connections (SNAT) - #SNAT_TO_SOURCE= # Use this IPv6 for outgoing connections (SNAT) - #SNAT6_TO_SOURCE= # Create or override an API key for the web UI @@ -486,6 +405,10 @@ MAILDIR_SUB=Maildir # SOGo session timeout in minutes SOGO_EXPIRE_SESSION=480 +# SOGo URL encryption key (exactly 16 characters, limited to A–Z, a–z, 0–9) +# This key is used to encrypt email addresses within SOGo URLs +SOGO_URL_ENCRYPTION_KEY=$(LC_ALL=C /dev/null | head -c 16) + # DOVECOT_MASTER_USER and DOVECOT_MASTER_PASS must both be provided. No special chars. # Empty by default to auto-generate master user and password on start. # User expands to DOVECOT_MASTER_USER@mailcow.local @@ -494,13 +417,6 @@ DOVECOT_MASTER_USER= # LEAVE EMPTY IF UNSURE DOVECOT_MASTER_PASS= -# Let's Encrypt registration contact information -# Optional: Leave empty for none -# This value is only used on first order! -# Setting it at a later point will require the following steps: -# https://docs.mailcow.email/troubleshooting/debug-reset_tls/ -ACME_CONTACT= - # WebAuthn device manufacturer verification # After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed # root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates @@ -513,6 +429,13 @@ WEBAUTHN_ONLY_TRUSTED_VENDORS=n # Otherwise it will work normally. SPAMHAUS_DQS_KEY= +# IPv6 Controller Section +# This variable controls the usage of IPv6 within mailcow. +# Can either be true or false | Defaults to true +# WARNING: MAKE SURE TO PROPERLY CONFIGURE IPv6 ON YOUR HOST FIRST BEFORE ENABLING THIS AS FAULTY CONFIGURATIONS CAN LEAD TO OPEN RELAYS! +# A COMPLETE DOCKER STACK REBUILD (compose down && compose up -d) IS NEEDED TO APPLY THIS. +ENABLE_IPV6=${IPV6_BOOL} + # Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n # CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost DISABLE_NETFILTER_ISOLATION_RULE=n @@ -592,5 +515,3 @@ else echo '?>' >> data/web/inc/app_info.inc.php echo -e "\e[33mCannot determine current git repository version...\e[0m" fi - -detect_bad_asn diff --git a/mailcow/helper-scripts/_cold-standby.sh b/mailcow/helper-scripts/_cold-standby.sh index bfda3ba..d4c3d0c 100755 --- a/mailcow/helper-scripts/_cold-standby.sh +++ b/mailcow/helper-scripts/_cold-standby.sh @@ -117,7 +117,7 @@ fi SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) source "${SCRIPT_DIR}/../mailcow.conf" COMPOSE_FILE="${SCRIPT_DIR}/../docker-compose.yml" -CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd 'A-Za-z-_') +CMPS_PRJ=$(echo ${COMPOSE_PROJECT_NAME} | tr -cd '0-9A-Za-z-_') SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' "${COMPOSE_FILE}") preflight_local_checks @@ -169,7 +169,7 @@ if ! ssh -o StrictHostKeyChecking=no \ -i "${REMOTE_SSH_KEY}" \ ${REMOTE_SSH_HOST} \ -p ${REMOTE_SSH_PORT} \ - ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" create 2>&1 ; then + "cd \"${SCRIPT_DIR}/../\" && ${COMPOSE_COMMAND} create 2>&1" ; then >&2 echo -e "\e[31m[ERR]\e[0m - Could not create networks, volumes and containers on remote" fi @@ -284,7 +284,7 @@ echo "OK" -i "${REMOTE_SSH_KEY}" \ ${REMOTE_SSH_HOST} \ -p ${REMOTE_SSH_PORT} \ - ${COMPOSE_COMMAND} -f "${SCRIPT_DIR}/../docker-compose.yml" pull --quiet 2>&1 ; then + "cd \"${SCRIPT_DIR}/../\" && ${COMPOSE_COMMAND} pull --quiet 2>&1" ; then >&2 echo -e "\e[31m[ERR]\e[0m - Could not pull images on remote" fi @@ -293,7 +293,7 @@ if ! ssh -o StrictHostKeyChecking=no \ -i "${REMOTE_SSH_KEY}" \ ${REMOTE_SSH_HOST} \ -p ${REMOTE_SSH_PORT} \ - ${SCRIPT_DIR}/../update.sh -f --gc ; then + "cd \"${SCRIPT_DIR}/../\" && ./update.sh -f --gc" ; then >&2 echo -e "\e[31m[ERR]\e[0m - Could not cleanup old images on remote" fi diff --git a/mailcow/helper-scripts/backup_and_restore.sh b/mailcow/helper-scripts/backup_and_restore.sh index c7615c2..2e65f13 100755 --- a/mailcow/helper-scripts/backup_and_restore.sh +++ b/mailcow/helper-scripts/backup_and_restore.sh @@ -91,6 +91,44 @@ if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then exit 1 fi +# Add image prefetch function +function prefetch_image() { + echo "Checking Docker image: ${DEBIAN_DOCKER_IMAGE}" + + # Get local image digest if it exists + local local_digest=$(docker image inspect ${DEBIAN_DOCKER_IMAGE} --format='{{index .RepoDigests 0}}' 2>/dev/null | cut -d'@' -f2) + + # Get remote image digest without pulling + local remote_digest=$(docker manifest inspect ${DEBIAN_DOCKER_IMAGE} 2>/dev/null | grep -oP '"digest":\s*"\K[^"]+' | head -1) + + if [[ -z "${remote_digest}" ]]; then + echo "Warning: Unable to check remote image" + if [[ -n "${local_digest}" ]]; then + echo "Using cached version" + echo + return 0 + else + echo "Error: Image ${DEBIAN_DOCKER_IMAGE} not found locally or remotely" + exit 1 + fi + fi + + if [[ "${local_digest}" != "${remote_digest}" ]]; then + echo "Image update available, pulling ${DEBIAN_DOCKER_IMAGE}" + if docker pull ${DEBIAN_DOCKER_IMAGE} 2>/dev/null; then + echo "Successfully pulled ${DEBIAN_DOCKER_IMAGE}" + else + echo "Error: Failed to pull ${DEBIAN_DOCKER_IMAGE}" + exit 1 + fi + else + echo "Image is up to date (${remote_digest:0:12}...)" + fi + echo +} + +# Prefetch the image early in the script +prefetch_image function backup() { DATE=$(date +"%Y-%m-%d-%H-%M-%S") @@ -110,32 +148,32 @@ function backup() { docker run --name mailcow-backup --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_vmail.tar.gz /vmail + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_vmail.tar.zst /vmail ;;& crypt|all) docker run --name mailcow-backup --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_crypt.tar.gz /crypt + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_crypt.tar.zst /crypt ;;& redis|all) docker exec $(docker ps -qf name=redis-mailcow) redis-cli -a ${REDISPASS} --no-auth-warning save docker run --name mailcow-backup --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_redis.tar.gz /redis + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_redis.tar.zst /redis ;;& rspamd|all) docker run --name mailcow-backup --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_rspamd.tar.gz /rspamd + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_rspamd.tar.zst /rspamd ;;& postfix|all) docker run --name mailcow-backup --rm \ -v ${BACKUP_LOCATION}/mailcow-${DATE}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:ro,z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="pigz --rsyncable -p ${THREADS}" -Pcvpf /backup/backup_postfix.tar.gz /postfix + ${DEBIAN_DOCKER_IMAGE} /bin/tar --warning='no-file-ignored' --use-compress-program="zstd --rsyncable -T${THREADS}" -Pcvpf /backup/backup_postfix.tar.zst /postfix ;;& mysql|all) SQLIMAGE=$(grep -iEo '(mysql|mariadb)\:.+' ${COMPOSE_FILE}) @@ -154,7 +192,7 @@ function backup() { ${SQLIMAGE} /bin/sh -c "mariabackup --host mysql --user root --password ${DBROOT} --backup --rsync --target-dir=/backup_mariadb ; \ mariabackup --prepare --target-dir=/backup_mariadb ; \ chown -R 999:999 /backup_mariadb ; \ - /bin/tar --warning='no-file-ignored' --use-compress-program='gzip --rsyncable' -Pcvpf /backup/backup_mariadb.tar.gz /backup_mariadb ;" + /bin/tar --warning='no-file-ignored' --use-compress-program='zstd --rsyncable' -Pcvpf /backup/backup_mariadb.tar.zst /backup_mariadb ;" fi ;;& --delete-days) @@ -170,6 +208,19 @@ function backup() { done } +function get_archive_info() { + local backup_name="$1" + local location="$2" + + if [[ -f "${location}/${backup_name}.tar.zst" ]]; then + echo "${backup_name}.tar.zst|zstd -d -T${THREADS}" + elif [[ -f "${location}/${backup_name}.tar.gz" ]]; then + echo "${backup_name}.tar.gz|pigz -d -p ${THREADS}" + else + echo "" + fi +} + function restore() { for bin in docker; do if [[ -z $(which ${bin}) ]]; then @@ -199,10 +250,17 @@ function restore() { case "$1" in vmail) docker stop $(docker ps -qf name=dovecot-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_vmail.tar.gz + ARCHIVE_INFO=$(get_archive_info "backup_vmail" "${RESTORE_LOCATION}") + if [[ -z "${ARCHIVE_INFO}" ]]; then + echo -e "\e[31mError: No backup file found for vmail (searched for .tar.zst and .tar.gz)\e[0m" + else + ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1) + DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_vmail-vol-1$):/vmail:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE} + fi docker start $(docker ps -aqf name=dovecot-mailcow) echo echo "In most cases it is not required to run a full resync, you can run the command printed below at any time after testing wether the restore process broke a mailbox:" @@ -218,31 +276,50 @@ function restore() { ;; redis) docker stop $(docker ps -qf name=redis-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_redis.tar.gz + ARCHIVE_INFO=$(get_archive_info "backup_redis" "${RESTORE_LOCATION}") + if [[ -z "${ARCHIVE_INFO}" ]]; then + echo -e "\e[31mError: No backup file found for redis (searched for .tar.zst and .tar.gz)\e[0m" + else + ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1) + DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_redis-vol-1$):/redis:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE} + fi docker start $(docker ps -aqf name=redis-mailcow) ;; crypt) docker stop $(docker ps -qf name=dovecot-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_crypt.tar.gz + ARCHIVE_INFO=$(get_archive_info "backup_crypt" "${RESTORE_LOCATION}") + if [[ -z "${ARCHIVE_INFO}" ]]; then + echo -e "\e[31mError: No backup file found for crypt (searched for .tar.zst and .tar.gz)\e[0m" + else + ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1) + DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_crypt-vol-1$):/crypt:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE} + fi docker start $(docker ps -aqf name=dovecot-mailcow) ;; rspamd) - if [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then + ARCHIVE_INFO=$(get_archive_info "backup_rspamd" "${RESTORE_LOCATION}") + if [[ -z "${ARCHIVE_INFO}" ]]; then + echo -e "\e[31mError: No backup file found for rspamd (searched for .tar.zst and .tar.gz)\e[0m" + elif [[ $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') == "" ]]; then echo -e "\e[33mCould not find a architecture signature of the loaded backup... Maybe the backup was done before the multiarch update?" sleep 2 echo -e "Continuing anyhow. If rspamd is crashing upon boot try remove the rspamd volume with docker volume rm ${CMPS_PRJ}_rspamd-vol-1 after you've stopped the stack.\e[0m" sleep 2 docker stop $(docker ps -qf name=rspamd-mailcow) + ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1) + DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2) docker run -i --name mailcow-backup --rm \ -v ${RESTORE_LOCATION}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE} docker start $(docker ps -aqf name=rspamd-mailcow) elif [[ $ARCH != $(find "${RESTORE_LOCATION}" \( -name '*x86*' -o -name '*aarch*' \) -exec basename {} \; | sed 's/^\.//' | sed 's/^\.//') ]]; then echo -e "\e[31mThe Architecture of the backed up mailcow OS is different then your restoring mailcow OS..." @@ -250,19 +327,28 @@ function restore() { echo -e "Skipping rspamd due to compatibility issues!\e[0m" else docker stop $(docker ps -qf name=rspamd-mailcow) + ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1) + DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2) docker run -i --name mailcow-backup --rm \ -v ${RESTORE_LOCATION}:/backup:z \ -v $(docker volume ls -qf name=^${CMPS_PRJ}_rspamd-vol-1$):/rspamd:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_rspamd.tar.gz + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE} docker start $(docker ps -aqf name=rspamd-mailcow) fi ;; postfix) docker stop $(docker ps -qf name=postfix-mailcow) - docker run -i --name mailcow-backup --rm \ - -v ${RESTORE_LOCATION}:/backup:z \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:z \ - ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -Pxvf /backup/backup_postfix.tar.gz + ARCHIVE_INFO=$(get_archive_info "backup_postfix" "${RESTORE_LOCATION}") + if [[ -z "${ARCHIVE_INFO}" ]]; then + echo -e "\e[31mError: No backup file found for postfix (searched for .tar.zst and .tar.gz)\e[0m" + else + ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1) + DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2) + docker run -i --name mailcow-backup --rm \ + -v ${RESTORE_LOCATION}:/backup:z \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_postfix-vol-1$):/postfix:z \ + ${DEBIAN_DOCKER_IMAGE} /bin/tar --use-compress-program="${DECOMPRESS_PROG}" -Pxvf /backup/${ARCHIVE_FILE} + fi docker start $(docker ps -aqf name=postfix-mailcow) ;; mysql|mariadb) @@ -305,14 +391,19 @@ function restore() { echo Restoring... && \ gunzip < backup/backup_mysql.gz | mysql -uroot && \ mysql -uroot -e SHUTDOWN;" - elif [[ -f "${RESTORE_LOCATION}/backup_mariadb.tar.gz" ]]; then - docker run --name mailcow-backup --rm \ - -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \ - --entrypoint= \ - -v ${RESTORE_LOCATION}:/backup:z \ - ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \ - /bin/rm -rf /backup_mariadb/* ; \ - /bin/tar -Pxvzf /backup/backup_mariadb.tar.gz" + else + ARCHIVE_INFO=$(get_archive_info "backup_mariadb" "${RESTORE_LOCATION}") + if [[ -n "${ARCHIVE_INFO}" ]]; then + ARCHIVE_FILE=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f1) + DECOMPRESS_PROG=$(echo "${ARCHIVE_INFO}" | cut -d'|' -f2) + docker run --name mailcow-backup --rm \ + -v $(docker volume ls -qf name=^${CMPS_PRJ}_mysql-vol-1$):/backup_mariadb/:rw,z \ + --entrypoint= \ + -v ${RESTORE_LOCATION}:/backup:z \ + ${SQLIMAGE} /bin/bash -c "shopt -s dotglob ; \ + /bin/rm -rf /backup_mariadb/* ; \ + /bin/tar --use-compress-program='${DECOMPRESS_PROG}' -Pxvf /backup/${ARCHIVE_FILE}" + fi fi echo "Modifying mailcow.conf..." source ${RESTORE_LOCATION}/mailcow.conf @@ -363,8 +454,8 @@ elif [[ ${1} == "restore" ]]; then fi echo "[ 0 ] - all" - # find all files in folder with *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .gz - FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//') + # find all files in folder with *.zst or *.gz extension, print their base names, remove backup_, remove .tar (if present), remove .zst/.gz + FILE_SELECTION[0]=$(find "${FOLDER_SELECTION[${input_sel}]}" -maxdepth 1 \( -type d -o -type f \) \( -name '*.zst' -o -name '*.gz' -o -name 'mysql' \) -printf '%f\n' | sed 's/backup_*//' | sed 's/\.[^.]*$//' | sed 's/\.[^.]*$//' | sort -u) for file in $(ls -f "${FOLDER_SELECTION[${input_sel}]}"); do if [[ ${file} =~ vmail ]]; then echo "[ ${i} ] - Mail directory (/var/vmail)" diff --git a/mailcow/helper-scripts/dev_tests/test_backup_and_restore.sh b/mailcow/helper-scripts/dev_tests/test_backup_and_restore.sh new file mode 100755 index 0000000..3e39d0d --- /dev/null +++ b/mailcow/helper-scripts/dev_tests/test_backup_and_restore.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash + +# Test script for backup_and_restore.sh +# Tests backward compatibility with .tar.gz and new .tar.zst format + +set -e + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BACKUP_IMAGE="${BACKUP_IMAGE:-ghcr.io/mailcow/backup:latest}" +TEST_DIR="/tmp/mailcow_backup_test_$$" +THREADS=2 + +echo "=== Mailcow Backup & Restore Test Suite ===" +echo "Test directory: ${TEST_DIR}" +echo "Backup image: ${BACKUP_IMAGE}" +echo "" + +# Cleanup function +cleanup() { + echo "Cleaning up test files..." + rm -rf "${TEST_DIR}" + docker rmi mailcow-backup-test 2>/dev/null || true +} +trap cleanup EXIT + +# Create test directory structure +mkdir -p "${TEST_DIR}"/{test_data,backup_zst,backup_gz,restore_zst,restore_gz,backup_large_zst,backup_large_gz} +echo "Test data for mailcow backup compatibility test" > "${TEST_DIR}/test_data/test.txt" +echo "Additional file to verify complete restore" > "${TEST_DIR}/test_data/test2.txt" + +# Build test backup image with zstd support +echo "=== Building backup image with zstd support ===" +docker build -t mailcow-backup-test "${SCRIPT_DIR}/../data/Dockerfiles/backup/" || { + echo "ERROR: Failed to build backup image" + exit 1 +} + +# Test 1: Create .tar.zst backup +echo "" +echo "=== Test 1: Creating .tar.zst backup ===" +docker run --rm \ + -w /data \ + -v "${TEST_DIR}/test_data:/data:ro" \ + -v "${TEST_DIR}/backup_zst:/backup" \ + mailcow-backup-test \ + /bin/tar --use-compress-program="zstd --rsyncable -T${THREADS}" \ + -cvpf /backup/backup_test.tar.zst . \ + > /dev/null +echo "✓ .tar.zst backup created: $(ls -lh ${TEST_DIR}/backup_zst/backup_test.tar.zst | awk '{print $5}')" + +# Test 2: Create .tar.gz backup +echo "" +echo "=== Test 2: Creating .tar.gz backup (legacy) ===" +docker run --rm \ + -w /data \ + -v "${TEST_DIR}/test_data:/data:ro" \ + -v "${TEST_DIR}/backup_gz:/backup" \ + mailcow-backup-test \ + /bin/tar --use-compress-program="pigz --rsyncable -p ${THREADS}" \ + -cvpf /backup/backup_test.tar.gz . \ + > /dev/null +echo "✓ .tar.gz backup created: $(ls -lh ${TEST_DIR}/backup_gz/backup_test.tar.gz | awk '{print $5}')" + +# Test 3: Test get_archive_info function +echo "" +echo "=== Test 3: Testing get_archive_info function ===" + +# Extract and test the function directly +get_archive_info() { + local backup_name="$1" + local location="$2" + + if [[ -f "${location}/${backup_name}.tar.zst" ]]; then + echo "${backup_name}.tar.zst|zstd -d -T${THREADS}" + elif [[ -f "${location}/${backup_name}.tar.gz" ]]; then + echo "${backup_name}.tar.gz|pigz -d -p ${THREADS}" + else + echo "" + fi +} + +# Test with .tar.zst +result=$(get_archive_info "backup_test" "${TEST_DIR}/backup_zst") +if [[ "${result}" =~ "zstd" ]]; then + echo "✓ Correctly detects .tar.zst and returns zstd decompressor" +else + echo "✗ Failed to detect .tar.zst" + exit 1 +fi + +# Test with .tar.gz +result=$(get_archive_info "backup_test" "${TEST_DIR}/backup_gz") +if [[ "${result}" =~ "pigz" ]]; then + echo "✓ Correctly detects .tar.gz and returns pigz decompressor" +else + echo "✗ Failed to detect .tar.gz" + exit 1 +fi + +# Test with no file +result=$(get_archive_info "backup_test" "${TEST_DIR}") +if [[ -z "${result}" ]]; then + echo "✓ Correctly returns empty when no backup file found" +else + echo "✗ Should return empty but got: ${result}" + exit 1 +fi + +# Test 4: Restore from .tar.zst +echo "" +echo "=== Test 4: Restoring from .tar.zst ===" +docker run --rm \ + -w /restore \ + -v "${TEST_DIR}/backup_zst:/backup:ro" \ + -v "${TEST_DIR}/restore_zst:/restore" \ + mailcow-backup-test \ + /bin/tar --use-compress-program="zstd -d -T${THREADS}" -xvpf /backup/backup_test.tar.zst \ + > /dev/null 2>&1 + +if [[ -f "${TEST_DIR}/restore_zst/test.txt" ]] && \ + [[ -f "${TEST_DIR}/restore_zst/test2.txt" ]]; then + echo "✓ Successfully restored from .tar.zst" +else + echo "✗ Failed to restore from .tar.zst" + ls -la "${TEST_DIR}/restore_zst/" || true + exit 1 +fi + +# Test 5: Restore from .tar.gz +echo "" +echo "=== Test 5: Restoring from .tar.gz (backward compatibility) ===" +docker run --rm \ + -w /restore \ + -v "${TEST_DIR}/backup_gz:/backup:ro" \ + -v "${TEST_DIR}/restore_gz:/restore" \ + mailcow-backup-test \ + /bin/tar --use-compress-program="pigz -d -p ${THREADS}" -xvpf /backup/backup_test.tar.gz \ + > /dev/null 2>&1 + +if [[ -f "${TEST_DIR}/restore_gz/test.txt" ]] && \ + [[ -f "${TEST_DIR}/restore_gz/test2.txt" ]]; then + echo "✓ Successfully restored from .tar.gz (backward compatible)" +else + echo "✗ Failed to restore from .tar.gz" + ls -la "${TEST_DIR}/restore_gz/" || true + exit 1 +fi + +# Test 6: Verify content integrity +echo "" +echo "=== Test 6: Verifying content integrity ===" +original_content=$(cat "${TEST_DIR}/test_data/test.txt") +zst_content=$(cat "${TEST_DIR}/restore_zst/test.txt") +gz_content=$(cat "${TEST_DIR}/restore_gz/test.txt") + +if [[ "${original_content}" == "${zst_content}" ]] && \ + [[ "${original_content}" == "${gz_content}" ]]; then + echo "✓ Content integrity verified for both formats" +else + echo "✗ Content mismatch detected" + exit 1 +fi + +# Test 7: Compare compression ratios +echo "" +echo "=== Test 7: Compression comparison ===" +zst_size=$(stat -f%z "${TEST_DIR}/backup_zst/backup_test.tar.zst" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_zst/backup_test.tar.zst") +gz_size=$(stat -f%z "${TEST_DIR}/backup_gz/backup_test.tar.gz" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_gz/backup_test.tar.gz") +improvement=$(echo "scale=2; (${gz_size} - ${zst_size}) * 100 / ${gz_size}" | bc) + +echo " Small files - .tar.gz size: ${gz_size} bytes" +echo " Small files - .tar.zst size: ${zst_size} bytes" +echo " Small files - Improvement: ${improvement}% smaller with zstd" + +# Test 8: Error handling - missing backup file +echo "" +echo "=== Test 8: Error handling - Missing backup file ===" +result=$(get_archive_info "nonexistent_backup" "${TEST_DIR}/backup_zst") +if [[ -z "${result}" ]]; then + echo "✓ Correctly handles missing backup files" +else + echo "✗ Should return empty for missing files" + exit 1 +fi + +# Test 9: Error handling - Empty directory +echo "" +echo "=== Test 9: Error handling - Empty directory ===" +mkdir -p "${TEST_DIR}/empty_dir" +result=$(get_archive_info "backup_test" "${TEST_DIR}/empty_dir") +if [[ -z "${result}" ]]; then + echo "✓ Correctly handles empty directories" +else + echo "✗ Should return empty for empty directories" + exit 1 +fi + +# Test 10: Priority test - .tar.zst preferred over .tar.gz +echo "" +echo "=== Test 10: Format priority - .tar.zst preferred ===" +mkdir -p "${TEST_DIR}/both_formats" +touch "${TEST_DIR}/both_formats/backup_test.tar.gz" +touch "${TEST_DIR}/both_formats/backup_test.tar.zst" +result=$(get_archive_info "backup_test" "${TEST_DIR}/both_formats") +if [[ "${result}" =~ "zstd" ]]; then + echo "✓ Correctly prefers .tar.zst when both formats exist" +else + echo "✗ Should prefer .tar.zst over .tar.gz" + exit 1 +fi + +# Test 11: Large file compression test +echo "" +echo "=== Test 11: Large file compression test ===" +mkdir -p "${TEST_DIR}/large_data" +# Create ~10MB of compressible data (log-like content) +for i in {1..50000}; do + echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: Processing email message $i from user@example.com to recipient@domain.com" >> "${TEST_DIR}/large_data/maillog.txt" + echo "[$(date '+%Y-%m-%d %H:%M:%S')] DEBUG: SMTP connection established from 192.168.1.$((i % 255))" >> "${TEST_DIR}/large_data/maillog.txt" +done 2>/dev/null + +# Get size (portable: works on Linux and macOS) +if du --version 2>/dev/null | grep -q GNU; then + original_size=$(du -sb "${TEST_DIR}/large_data" | cut -f1) +else + # macOS + original_size=$(find "${TEST_DIR}/large_data" -type f -exec stat -f%z {} \; | awk '{sum+=$1} END {print sum}') +fi +echo " Original data size: $(echo "scale=2; ${original_size} / 1024 / 1024" | bc) MB" + +# Backup with zstd +docker run --rm \ + -w /data \ + -v "${TEST_DIR}/large_data:/data:ro" \ + -v "${TEST_DIR}/backup_large_zst:/backup" \ + mailcow-backup-test \ + /bin/tar --use-compress-program="zstd --rsyncable -T${THREADS}" \ + -cvpf /backup/backup_large.tar.zst . \ + > /dev/null 2>&1 + +# Backup with pigz +docker run --rm \ + -w /data \ + -v "${TEST_DIR}/large_data:/data:ro" \ + -v "${TEST_DIR}/backup_large_gz:/backup" \ + mailcow-backup-test \ + /bin/tar --use-compress-program="pigz --rsyncable -p ${THREADS}" \ + -cvpf /backup/backup_large.tar.gz . \ + > /dev/null 2>&1 + +zst_large_size=$(stat -f%z "${TEST_DIR}/backup_large_zst/backup_large.tar.zst" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_large_zst/backup_large.tar.zst" 2>/dev/null || echo "0") +gz_large_size=$(stat -f%z "${TEST_DIR}/backup_large_gz/backup_large.tar.gz" 2>/dev/null || stat -c%s "${TEST_DIR}/backup_large_gz/backup_large.tar.gz" 2>/dev/null || echo "0") + +if [[ ${zst_large_size} -gt 0 ]] && [[ ${gz_large_size} -gt 0 ]]; then + large_improvement=$(echo "scale=2; (${gz_large_size} - ${zst_large_size}) * 100 / ${gz_large_size}" | bc) + + echo " .tar.gz compressed: $(echo "scale=2; ${gz_large_size} / 1024 / 1024" | bc) MB" + echo " .tar.zst compressed: $(echo "scale=2; ${zst_large_size} / 1024 / 1024" | bc) MB" + echo " Improvement: ${large_improvement}% smaller with zstd" +else + echo " ✗ Failed to get file sizes" + exit 1 +fi + +if [[ $(echo "${large_improvement} > 0" | bc) -eq 1 ]]; then + echo "✓ zstd provides better compression on realistic data" +else + echo "⚠ zstd compression similar or worse than gzip (unusual but not critical)" +fi + +# Test 12: Thread scaling test +echo "" +echo "=== Test 12: Multi-threading verification ===" +# This test verifies that different thread counts work (not measuring speed difference) +for thread_count in 1 4; do + THREADS=${thread_count} + result=$(get_archive_info "backup_test" "${TEST_DIR}/backup_zst") + if [[ "${result}" =~ "-T${thread_count}" ]]; then + echo "✓ Thread count ${thread_count} correctly configured" + else + echo "✗ Thread count not properly applied" + exit 1 + fi +done + +echo "" +echo "=== All tests passed! ===" +echo "" +echo "Summary:" +echo " ✓ zstd compression working" +echo " ✓ pigz compression working (legacy)" +echo " ✓ zstd decompression working" +echo " ✓ pigz decompression working (backward compatible)" +echo " ✓ Archive detection working" +echo " ✓ Content integrity verified" +echo " ✓ Format priority correct (.tar.zst preferred)" +echo " ✓ Error handling for missing files" +echo " ✓ Error handling for empty directories" +echo " ✓ Multi-threading configuration verified" +echo " ✓ Large file compression: ${large_improvement}% improvement" +echo " ✓ Small file compression: ${improvement}% improvement" diff --git a/mailcow/helper-scripts/update_postscreen_whitelist.sh b/mailcow/helper-scripts/update_postscreen_whitelist.sh index 04335bd..dda64b2 100644 --- a/mailcow/helper-scripts/update_postscreen_whitelist.sh +++ b/mailcow/helper-scripts/update_postscreen_whitelist.sh @@ -6,9 +6,10 @@ SPFTOOLS_DIR=${WORKING_DIR}/spf-tools POSTWHITE_DIR=${WORKING_DIR}/postwhite POSTWHITE_CONF=${POSTWHITE_DIR}/postwhite.conf -CUSTOM_HOSTS='"web.de gmx.net mail.de freenet.de arcor.de unity-mail.de"' +CUSTOM_HOSTS='"web.de gmx.net mail.de freenet.de arcor.de unity-mail.de protonmail.ch ionos.com strato.com t-online.de"' STATIC_HOSTS=( - "194.25.134.0/24 permit # t-online.de" + "49.12.4.251 permit # checks.mailcow.email" + "2a01:4f8:c17:7906::10 permit # checks.mailcow.email" ) mkdir ${SCRIPT_DIR}/postwhite_tmp diff --git a/mailcow/update.sh b/mailcow/update.sh index ea2fcf1..74eb8ff 100755 --- a/mailcow/update.sh +++ b/mailcow/update.sh @@ -2,763 +2,53 @@ ############## Begin Function Section ############## -check_online_status() { - CHECK_ONLINE_DOMAINS=('https://github.com' 'https://hub.docker.com') - for domain in "${CHECK_ONLINE_DOMAINS[@]}"; do - if timeout 6 curl --head --silent --output /dev/null ${domain}; then - return 0 - fi - done - return 1 -} +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +MAILCOW_CONF="${SCRIPT_DIR}/mailcow.conf" -prefetch_images() { - [[ -z ${BRANCH} ]] && { echo -e "\e[33m\nUnknown branch...\e[0m"; exit 1; } - git fetch origin #${BRANCH} - while read image; do - if [[ "${image}" == "robbertkl/ipv6nat" ]]; then - if ! grep -qi "ipv6nat-mailcow" docker-compose.yml || grep -qi "enable_ipv6: false" docker-compose.yml; then - continue - fi - fi - RET_C=0 - until docker pull "${image}"; do - RET_C=$((RET_C + 1)) - echo -e "\e[33m\nError pulling $image, retrying...\e[0m" - [ ${RET_C} -gt 3 ] && { echo -e "\e[31m\nToo many failed retries, exiting\e[0m"; exit 1; } - sleep 1 - done - done < <(git show "origin/${BRANCH}:docker-compose.yml" | grep "image:" | awk '{ gsub("image:","", $3); print $2 }') -} - -docker_garbage() { - SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - IMGS_TO_DELETE=() - - declare -A IMAGES_INFO - COMPOSE_IMAGES=($(grep -oP "image: \K(ghcr\.io/)?mailcow.+" "${SCRIPT_DIR}/docker-compose.yml")) - - for existing_image in $(docker images --format "{{.ID}}:{{.Repository}}:{{.Tag}}" | grep -E '(mailcow/|ghcr\.io/mailcow/)'); do - ID=$(echo "$existing_image" | cut -d ':' -f 1) - REPOSITORY=$(echo "$existing_image" | cut -d ':' -f 2) - TAG=$(echo "$existing_image" | cut -d ':' -f 3) - - if [[ "$REPOSITORY" == "mailcow/backup" || "$REPOSITORY" == "ghcr.io/mailcow/backup" ]]; then - if [[ "$TAG" != "" ]]; then - continue - fi - fi - - if [[ " ${COMPOSE_IMAGES[@]} " =~ " ${REPOSITORY}:${TAG} " ]]; then - continue - else - IMGS_TO_DELETE+=("$ID") - IMAGES_INFO["$ID"]="$REPOSITORY:$TAG" - fi - done - - if [[ ! -z ${IMGS_TO_DELETE[*]} ]]; then - echo "The following unused mailcow images were found:" - for id in "${IMGS_TO_DELETE[@]}"; do - echo " ${IMAGES_INFO[$id]} ($id)" - done - - if [ -z "$FORCE" ]; then - read -r -p "Do you want to delete them to free up some space? [y/N] " response - if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - docker rmi ${IMGS_TO_DELETE[*]} - else - echo "OK, skipped." - fi - else - echo "Running in forced mode! Force removing old mailcow images..." - docker rmi ${IMGS_TO_DELETE[*]} - fi - echo -e "\e[32mFurther cleanup...\e[0m" - echo "If you want to cleanup further garbage collected by Docker, please make sure all containers are up and running before cleaning your system by executing \"docker system prune\"" - fi -} - -in_array() { - local e match="$1" - shift - for e; do [[ "$e" == "$match" ]] && return 0; done - return 1 -} - -migrate_docker_nat() { - NAT_CONFIG='{"ipv6":true,"fixed-cidr-v6":"fd00:dead:beef:c0::/80","experimental":true,"ip6tables":true}' - # Min Docker version - DOCKERV_REQ=20.10.2 - # Current Docker version - DOCKERV_CUR=$(docker version -f '{{.Server.Version}}') - if grep -qi "ipv6nat-mailcow" docker-compose.yml && grep -qi "enable_ipv6: true" docker-compose.yml; then - echo -e "\e[32mNative IPv6 implementation available.\e[0m" - echo "This will enable experimental features in the Docker daemon and configure Docker to do the IPv6 NATing instead of ipv6nat-mailcow." - echo '!!! This step is recommended !!!' - echo "mailcow will try to roll back the changes if starting Docker fails after modifying the daemon.json configuration file." - read -r -p "Should we try to enable the native IPv6 implementation in Docker now (recommended)? [y/N] " dockernatresponse - if [[ ! "${dockernatresponse}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - echo "OK, skipping this step." - return 0 - fi - fi - # Sort versions and check if we are running a newer or equal version to req - if [ $(printf "${DOCKERV_REQ}\n${DOCKERV_CUR}" | sort -V | tail -n1) == "${DOCKERV_CUR}" ]; then - # If Dockerd daemon json exists - if [ -s /etc/docker/daemon.json ]; then - IFS=',' read -r -a dockerconfig <<< $(cat /etc/docker/daemon.json | tr -cd '[:alnum:],') - if ! in_array ipv6true "${dockerconfig[@]}" || \ - ! in_array experimentaltrue "${dockerconfig[@]}" || \ - ! in_array ip6tablestrue "${dockerconfig[@]}" || \ - ! grep -qi "fixed-cidr-v6" /etc/docker/daemon.json; then - echo -e "\e[33mWarning:\e[0m You seem to have modified the /etc/docker/daemon.json configuration by yourself and not fully/correctly activated the native IPv6 NAT implementation." - echo "You will need to merge your existing configuration manually or fix/delete the existing daemon.json configuration before trying the update process again." - echo -e "Please merge the following content and restart the Docker daemon:\n" - echo "${NAT_CONFIG}" - return 1 - fi - else - echo "Working on IPv6 NAT, please wait..." - echo "${NAT_CONFIG}" > /etc/docker/daemon.json - ip6tables -F -t nat - [[ -e /etc/rc.conf ]] && rc-service docker restart || systemctl restart docker.service - if [[ $? -ne 0 ]]; then - echo -e "\e[31mError:\e[0m Failed to activate IPv6 NAT! Reverting and exiting." - rm /etc/docker/daemon.json - if [[ -e /etc/rc.conf ]]; then - rc-service docker restart - else - systemctl reset-failed docker.service - systemctl restart docker.service - fi - return 1 - fi - fi - # Removing legacy container - sed -i '/ipv6nat-mailcow:$/,/^$/d' docker-compose.yml - if [ -s docker-compose.override.yml ]; then - sed -i '/ipv6nat-mailcow:$/,/^$/d' docker-compose.override.yml - if [[ "$(cat docker-compose.override.yml | sed '/^\s*$/d' | wc -l)" == "2" ]]; then - mv docker-compose.override.yml docker-compose.override.yml_backup - fi - fi - echo -e "\e[32mGreat! \e[0mNative IPv6 NAT is active.\e[0m" +# Ensure the script is run from the directory that contains mailcow.conf +if [ ! -f "${PWD}/mailcow.conf" ]; then + if [ -f "${SCRIPT_DIR}/mailcow.conf" ]; then + echo -e "\e[33mPlease run this script directly from the mailcow installation directory:\e[0m" + echo -e " \e[36mcd ${SCRIPT_DIR} && ./update.sh\e[0m" + exit 1 else - echo -e "\e[31mPlease upgrade Docker to version ${DOCKERV_REQ} or above.\e[0m" - return 0 - fi -} - -remove_obsolete_nginx_ports() { - # Removing obsolete docker-compose.override.yml - for override in docker-compose.override.yml docker-compose.override.yaml; do - if [ -s $override ] ; then - if cat $override | grep nginx-mailcow > /dev/null 2>&1; then - if cat $override | grep -E '(\[::])' > /dev/null 2>&1; then - if cat $override | grep -w 80:80 > /dev/null 2>&1 && cat $override | grep -w 443:443 > /dev/null 2>&1 ; then - echo -e "\e[33mBacking up ${override} to preserve custom changes...\e[0m" - echo -e "\e[33m!!! Manual Merge needed (if other overrides are set) !!!\e[0m" - sleep 3 - cp $override ${override}_backup - sed -i '/nginx-mailcow:$/,/^$/d' $override - echo -e "\e[33mRemoved obsolete NGINX IPv6 Bind from original override File.\e[0m" - if [[ "$(cat $override | sed '/^\s*$/d' | wc -l)" == "2" ]]; then - mv $override ${override}_empty - echo -e "\e[31m${override} is empty. Renamed it to ensure mailcow is startable.\e[0m" - fi - fi - fi - fi - fi - done -} - -detect_docker_compose_command(){ -if ! [[ "${DOCKER_COMPOSE_VERSION}" =~ ^(native|standalone)$ ]]; then - if docker compose > /dev/null 2>&1; then - if docker compose version --short | grep -e "^2." -e "^v2." > /dev/null 2>&1; then - DOCKER_COMPOSE_VERSION=native - COMPOSE_COMMAND="docker compose" - echo -e "\e[33mFound Docker Compose Plugin (native).\e[0m" - echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to native\e[0m" - sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf" - sleep 2 - echo -e "\e[33mNotice: You'll have to update this Compose Version via your Package Manager manually!\e[0m" - else - echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" - echo -e "\e[31mPlease update/install it manually regarding to this doc site: https://docs.mailcow.email/install/\e[0m" - exit 1 - fi - elif docker-compose > /dev/null 2>&1; then - if ! [[ $(alias docker-compose 2> /dev/null) ]] ; then - if docker-compose version --short | grep "^2." > /dev/null 2>&1; then - DOCKER_COMPOSE_VERSION=standalone - COMPOSE_COMMAND="docker-compose" - echo -e "\e[33mFound Docker Compose Standalone.\e[0m" - echo -e "\e[33mSetting the DOCKER_COMPOSE_VERSION Variable to standalone\e[0m" - sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf" - sleep 2 - echo -e "\e[33mNotice: For an automatic update of docker-compose please use the update_compose.sh scripts located at the helper-scripts folder.\e[0m" - else - echo -e "\e[31mCannot find Docker Compose with a Version Higher than 2.X.X.\e[0m" - echo -e "\e[31mPlease update/install regarding to this doc site: https://docs.mailcow.email/install/\e[0m" - exit 1 - fi - fi - - else - echo -e "\e[31mCannot find Docker Compose.\e[0m" - echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" + echo -e "\e[31mmailcow.conf not found in current directory or script directory (\e[36m${SCRIPT_DIR}\e[31m).\e[0m" + echo -e "\e[33mRun this script directly from your mailcow installation directory.\e[0m" exit 1 fi - -elif [ "${DOCKER_COMPOSE_VERSION}" == "native" ]; then - COMPOSE_COMMAND="docker compose" - # Check if Native Compose works and has not been deleted - if ! $COMPOSE_COMMAND > /dev/null 2>&1; then - # IF it not exists/work anymore try the other command - COMPOSE_COMMAND="docker-compose" - if ! $COMPOSE_COMMAND > /dev/null 2>&1 || ! $COMPOSE_COMMAND --version | grep "^2." > /dev/null 2>&1; then - # IF it cannot find Standalone in > 2.X, then script stops - echo -e "\e[31mCannot find Docker Compose or the Version is lower then 2.X.X.\e[0m" - echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" - exit 1 - fi - # If it finds the standalone Plugin it will use this instead and change the mailcow.conf Variable accordingly - echo -e "\e[31mFound different Docker Compose Version then declared in mailcow.conf!\e[0m" - echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable from native to standalone\e[0m" - sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=standalone/' "$SCRIPT_DIR/mailcow.conf" - sleep 2 - fi - - -elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then - COMPOSE_COMMAND="docker-compose" - # Check if Standalone Compose works and has not been deleted - if ! $COMPOSE_COMMAND > /dev/null 2>&1 && ! $COMPOSE_COMMAND --version > /dev/null 2>&1 | grep "^2." > /dev/null 2>&1; then - # IF it not exists/work anymore try the other command - COMPOSE_COMMAND="docker compose" - if ! $COMPOSE_COMMAND > /dev/null 2>&1; then - # IF it cannot find Native in > 2.X, then script stops - echo -e "\e[31mCannot find Docker Compose.\e[0m" - echo -e "\e[31mPlease install it regarding to this doc site: https://docs.mailcow.email/install/\e[0m" - exit 1 - fi - # If it finds the native Plugin it will use this instead and change the mailcow.conf Variable accordingly - echo -e "\e[31mFound different Docker Compose Version then declared in mailcow.conf!\e[0m" - echo -e "\e[31mSetting the DOCKER_COMPOSE_VERSION Variable from standalone to native\e[0m" - sed -i 's/^DOCKER_COMPOSE_VERSION=.*/DOCKER_COMPOSE_VERSION=native/' "$SCRIPT_DIR/mailcow.conf" - sleep 2 - fi fi -} +BRANCH="$(cd "${SCRIPT_DIR}" && git rev-parse --abbrev-ref HEAD)" -detect_bad_asn() { - echo -e "\e[33mDetecting if your IP is listed on Spamhaus Bad ASN List...\e[0m" - response=$(curl --connect-timeout 15 --max-time 30 -s -o /dev/null -w "%{http_code}" "https://asn-check.mailcow.email") - if [ "$response" -eq 503 ]; then - if [ -z "$SPAMHAUS_DQS_KEY" ]; then - echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" - echo -e "\e[33mmailcow did not detected a value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf!\e[0m" - sleep 2 - echo "" - echo -e "\e[33mTo use the Spamhaus DNS Blocklists again, you will need to create a FREE account for their Data Query Service (DQS) at: https://www.spamhaus.com/free-trial/sign-up-for-a-free-data-query-service-account\e[0m" - echo -e "\e[33mOnce done, enter your DQS API key in mailcow.conf and mailcow will do the rest for you!\e[0m" - echo "" - sleep 2 +MODULE_DIR="${SCRIPT_DIR}/_modules" +# Calculate hash before fetch +if [[ -d "${MODULE_DIR}" && -n "$(ls -A "${MODULE_DIR}" 2>/dev/null)" ]]; then + MODULES_HASH_BEFORE=$(find "${MODULE_DIR}" -type f -exec sha256sum {} \; 2>/dev/null | sort | sha256sum | awk '{print $1}') +else + MODULES_HASH_BEFORE="EMPTY" +fi - else - echo -e "\e[33mYour server's public IP uses an AS that is blocked by Spamhaus to use their DNS public blocklists for Postfix.\e[0m" - echo -e "\e[32mmailcow detected a Value for the variable SPAMHAUS_DQS_KEY inside mailcow.conf. Postfix will use DQS with the given API key...\e[0m" - fi - elif [ "$response" -eq 200 ]; then - echo -e "\e[33mCheck completed! Your IP is \e[32mclean\e[0m" - elif [ "$response" -eq 429 ]; then - echo -e "\e[33mCheck completed! \e[31mYour IP seems to be rate limited on the ASN Check service... please try again later!\e[0m" - else - echo -e "\e[31mCheck failed! \e[0mMaybe a DNS or Network problem?\e[0m" - fi -} +echo -e "\e[33mFetching latest _modules from origin/${BRANCH}…\e[0m" +git fetch origin "${BRANCH}" +git checkout "origin/${BRANCH}" -- _modules -fix_broken_dnslist_conf() { +if [[ ! -d "${MODULE_DIR}" || -z "$(ls -A "${MODULE_DIR}")" ]]; then + echo -e "\e[31mError: _modules is still missing or empty after fetch!\e[0m" + exit 2 +fi -# Fixing issue: #6143. To be removed in a later patch +# Calculate hash after fetch +MODULES_HASH_AFTER=$(find "${MODULE_DIR}" -type f -exec sha256sum {} \; 2>/dev/null | sort | sha256sum | awk '{print $1}') - local file="${SCRIPT_DIR}/data/conf/postfix/dns_blocklists.cf" - # Check if the file exists - if [[ ! -f "$file" ]]; then - return 1 - fi +# Check if modules changed +if [[ "${MODULES_HASH_BEFORE}" != "${MODULES_HASH_AFTER}" ]]; then + echo -e "\e[33m_modules have been updated. Please restart the update script.\e[0m" + exit 2 +fi - # Check if the file contains the autogenerated comment - if grep -q "# Autogenerated by mailcow" "$file"; then - # Ask the user if custom changes were made - echo -e "\e[91mWARNING!!! \e[31mAn old version of dns_blocklists.cf has been detected which may cause a broken postfix upon startup (see: https://github.com/mailcow/mailcow-dockerized/issues/6143)...\e[0m" - echo -e "\e[31mIf you have any custom settings in there you might copy it away and adapt the changes after the file is regenerated...\e[0m" - read -p "Do you want to delete the file now and let mailcow regenerate it properly? [y/n]" response - if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - rm "$file" - echo -e "\e[32mdns_blocklists.cf has been deleted and will be properly regenerated" - return 0 - else - echo -e "\e[35mOk, not deleting it! Please make sure you take a look at postfix upon start then..." - return 2 - fi - fi - -} - -adapt_new_options() { - - CONFIG_ARRAY=( - "SKIP_LETS_ENCRYPT" - "SKIP_SOGO" - "USE_WATCHDOG" - "WATCHDOG_NOTIFY_EMAIL" - "WATCHDOG_NOTIFY_WEBHOOK" - "WATCHDOG_NOTIFY_WEBHOOK_BODY" - "WATCHDOG_NOTIFY_BAN" - "WATCHDOG_NOTIFY_START" - "WATCHDOG_EXTERNAL_CHECKS" - "WATCHDOG_SUBJECT" - "SKIP_CLAMD" - "SKIP_IP_CHECK" - "ADDITIONAL_SAN" - "DOVEADM_PORT" - "IPV4_NETWORK" - "IPV6_NETWORK" - "LOG_LINES" - "SNAT_TO_SOURCE" - "SNAT6_TO_SOURCE" - "COMPOSE_PROJECT_NAME" - "DOCKER_COMPOSE_VERSION" - "SQL_PORT" - "API_KEY" - "API_KEY_READ_ONLY" - "API_ALLOW_FROM" - "MAILDIR_GC_TIME" - "MAILDIR_SUB" - "ACL_ANYONE" - "FTS_HEAP" - "FTS_PROCS" - "SKIP_FTS" - "ENABLE_SSL_SNI" - "ALLOW_ADMIN_EMAIL_LOGIN" - "SKIP_HTTP_VERIFICATION" - "SOGO_EXPIRE_SESSION" - "REDIS_PORT" - "DOVECOT_MASTER_USER" - "DOVECOT_MASTER_PASS" - "MAILCOW_PASS_SCHEME" - "ADDITIONAL_SERVER_NAMES" - "ACME_CONTACT" - "WATCHDOG_VERBOSE" - "WEBAUTHN_ONLY_TRUSTED_VENDORS" - "SPAMHAUS_DQS_KEY" - "SKIP_UNBOUND_HEALTHCHECK" - "DISABLE_NETFILTER_ISOLATION_RULE" - "HTTP_REDIRECT" - ) - - sed -i --follow-symlinks '$a\' mailcow.conf - for option in ${CONFIG_ARRAY[@]}; do - if [[ ${option} == "ADDITIONAL_SAN" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "${option}=" >> mailcow.conf - fi - elif [[ ${option} == "COMPOSE_PROJECT_NAME" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "COMPOSE_PROJECT_NAME=mailcowdockerized" >> mailcow.conf - fi - elif [[ ${option} == "DOCKER_COMPOSE_VERSION" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "# Used Docker Compose version" >> mailcow.conf - echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf - echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf - echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf - echo "# Please be aware that at least one of those variants should be installed on your maschine or mailcow will fail." >> mailcow.conf - echo "" >> mailcow.conf - echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf - fi - elif [[ ${option} == "DOVEADM_PORT" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf - fi - elif [[ ${option} == "WATCHDOG_NOTIFY_EMAIL" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "WATCHDOG_NOTIFY_EMAIL=" >> mailcow.conf - fi - elif [[ ${option} == "LOG_LINES" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf - echo "LOG_LINES=9999" >> mailcow.conf - fi - elif [[ ${option} == "IPV4_NETWORK" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf - echo "IPV4_NETWORK=172.22.1" >> mailcow.conf - fi - elif [[ ${option} == "IPV6_NETWORK" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Internal IPv6 subnet in fc00::/7' >> mailcow.conf - echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf - fi - elif [[ ${option} == "SQL_PORT" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Bind SQL to 127.0.0.1 on port 13306' >> mailcow.conf - echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf - fi - elif [[ ${option} == "API_KEY" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Create or override API key for web UI' >> mailcow.conf - echo "#API_KEY=" >> mailcow.conf - fi - elif [[ ${option} == "API_KEY_READ_ONLY" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Create or override read-only API key for web UI' >> mailcow.conf - echo "#API_KEY_READ_ONLY=" >> mailcow.conf - fi - elif [[ ${option} == "API_ALLOW_FROM" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Must be set for API_KEY to be active' >> mailcow.conf - echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf - echo "#API_ALLOW_FROM=" >> mailcow.conf - fi - elif [[ ${option} == "SNAT_TO_SOURCE" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf - echo "#SNAT_TO_SOURCE=" >> mailcow.conf - fi - elif [[ ${option} == "SNAT6_TO_SOURCE" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf - echo "#SNAT6_TO_SOURCE=" >> mailcow.conf - fi - elif [[ ${option} == "MAILDIR_GC_TIME" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Garbage collector cleanup' >> mailcow.conf - echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf - echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf - echo '# Check interval is hourly' >> mailcow.conf - echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf - fi - elif [[ ${option} == "ACL_ANYONE" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Set this to "allow" to enable the anyone pseudo user. Disabled by default.' >> mailcow.conf - echo '# When enabled, ACL can be created, that apply to "All authenticated users"' >> mailcow.conf - echo '# This should probably only be activated on mail hosts, that are used exclusivly by one organisation.' >> mailcow.conf - echo '# Otherwise a user might share data with too many other users.' >> mailcow.conf - echo 'ACL_ANYONE=disallow' >> mailcow.conf - fi - elif [[ ${option} == "FTS_HEAP" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Dovecot Indexing (FTS) Process maximum heap size in MB, there is no recommendation, please see Dovecot docs.' >> mailcow.conf - echo '# Flatcurve is used as FTS Engine. It is supposed to be pretty efficient in CPU and RAM consumption.' >> mailcow.conf - echo '# Please always monitor your Resource consumption!' >> mailcow.conf - echo "FTS_HEAP=128" >> mailcow.conf - fi - elif [[ ${option} == "SKIP_FTS" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Skip FTS (Fulltext Search) for Dovecot on low-memory, low-threaded systems or if you simply want to disable it.' >> mailcow.conf - echo "# Dovecot inside mailcow use Flatcurve as FTS Backend." >> mailcow.conf - echo "SKIP_FTS=y" >> mailcow.conf - fi - elif [[ ${option} == "FTS_PROCS" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Controls how many processes the Dovecot indexing process can spawn at max.' >> mailcow.conf - echo '# Too many indexing processes can use a lot of CPU and Disk I/O' >> mailcow.conf - echo '# Please visit: https://doc.dovecot.org/configuration_manual/service_configuration/#indexer-worker for more informations' >> mailcow.conf - echo "FTS_PROCS=1" >> mailcow.conf - fi - elif [[ ${option} == "ENABLE_SSL_SNI" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Create seperate certificates for all domains - y/n' >> mailcow.conf - echo '# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames' >> mailcow.conf - echo '# see https://wiki.dovecot.org/SSL/SNIClientSupport' >> mailcow.conf - echo "ENABLE_SSL_SNI=n" >> mailcow.conf - fi - elif [[ ${option} == "SKIP_SOGO" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n' >> mailcow.conf - echo "SKIP_SOGO=n" >> mailcow.conf - fi - elif [[ ${option} == "MAILDIR_SUB" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf - echo "#MAILDIR_SUB=Maildir" >> mailcow.conf - echo "MAILDIR_SUB=" >> mailcow.conf - fi - elif [[ ${option} == "WATCHDOG_NOTIFY_WEBHOOK" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Send notifications to a webhook URL that receives a POST request with the content type "application/json".' >> mailcow.conf - echo '# You can use this to send notifications to services like Discord, Slack and others.' >> mailcow.conf - echo '#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' >> mailcow.conf - fi - elif [[ ${option} == "WATCHDOG_NOTIFY_WEBHOOK_BODY" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# JSON body included in the webhook POST request. Needs to be in single quotes.' >> mailcow.conf - echo '# Following variables are available: SUBJECT, BODY' >> mailcow.conf - WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}' - echo "#WATCHDOG_NOTIFY_WEBHOOK_BODY='${WEBHOOK_BODY}'" >> mailcow.conf - fi - elif [[ ${option} == "WATCHDOG_NOTIFY_BAN" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf - echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf - fi - elif [[ ${option} == "WATCHDOG_NOTIFY_START" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Send a notification when the watchdog is started.' >> mailcow.conf - echo "WATCHDOG_NOTIFY_START=y" >> mailcow.conf - fi - elif [[ ${option} == "WATCHDOG_SUBJECT" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.' >> mailcow.conf - echo "#WATCHDOG_SUBJECT=" >> mailcow.conf - fi - elif [[ ${option} == "WATCHDOG_EXTERNAL_CHECKS" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.' >> mailcow.conf - echo '# No data is collected. Opt-in and anonymous.' >> mailcow.conf - echo '# Will only work with unmodified mailcow setups.' >> mailcow.conf - echo "WATCHDOG_EXTERNAL_CHECKS=n" >> mailcow.conf - fi - elif [[ ${option} == "SOGO_EXPIRE_SESSION" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# SOGo session timeout in minutes' >> mailcow.conf - echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf - fi - elif [[ ${option} == "REDIS_PORT" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "REDIS_PORT=127.0.0.1:7654" >> mailcow.conf - fi - elif [[ ${option} == "DOVECOT_MASTER_USER" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# DOVECOT_MASTER_USER and _PASS must _both_ be provided. No special chars.' >> mailcow.conf - echo '# Empty by default to auto-generate master user and password on start.' >> mailcow.conf - echo '# User expands to DOVECOT_MASTER_USER@mailcow.local' >> mailcow.conf - echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf - echo "DOVECOT_MASTER_USER=" >> mailcow.conf - fi - elif [[ ${option} == "DOVECOT_MASTER_PASS" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf - echo "DOVECOT_MASTER_PASS=" >> mailcow.conf - fi - elif [[ ${option} == "MAILCOW_PASS_SCHEME" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Password hash algorithm' >> mailcow.conf - echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf - echo '# see https://docs.mailcow.email/models/model-passwd/' >> mailcow.conf - echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf - fi - elif [[ ${option} == "ADDITIONAL_SERVER_NAMES" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Additional server names for mailcow UI' >> mailcow.conf - echo '#' >> mailcow.conf - echo '# Specify alternative addresses for the mailcow UI to respond to' >> mailcow.conf - echo '# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.' >> mailcow.conf - echo '# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.' >> mailcow.conf - echo '# You can understand this as server_name directive in Nginx.' >> mailcow.conf - echo '# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f' >> mailcow.conf - echo 'ADDITIONAL_SERVER_NAMES=' >> mailcow.conf - fi - elif [[ ${option} == "ACME_CONTACT" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Lets Encrypt registration contact information' >> mailcow.conf - echo '# Optional: Leave empty for none' >> mailcow.conf - echo '# This value is only used on first order!' >> mailcow.conf - echo '# Setting it at a later point will require the following steps:' >> mailcow.conf - echo '# https://docs.mailcow.email/troubleshooting/debug-reset_tls/' >> mailcow.conf - echo 'ACME_CONTACT=' >> mailcow.conf - fi - elif [[ ${option} == "WEBAUTHN_ONLY_TRUSTED_VENDORS" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "# WebAuthn device manufacturer verification" >> mailcow.conf - echo '# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed' >> mailcow.conf - echo '# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates' >> mailcow.conf - echo 'WEBAUTHN_ONLY_TRUSTED_VENDORS=n' >> mailcow.conf - fi - elif [[ ${option} == "SPAMHAUS_DQS_KEY" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "# Spamhaus Data Query Service Key" >> mailcow.conf - echo '# Optional: Leave empty for none' >> mailcow.conf - echo '# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist.' >> mailcow.conf - echo '# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS.' >> mailcow.conf - echo '# Otherwise it will work as usual.' >> mailcow.conf - echo 'SPAMHAUS_DQS_KEY=' >> mailcow.conf - fi - elif [[ ${option} == "WATCHDOG_VERBOSE" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Enable watchdog verbose logging' >> mailcow.conf - echo 'WATCHDOG_VERBOSE=n' >> mailcow.conf - fi - elif [[ ${option} == "SKIP_UNBOUND_HEALTHCHECK" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n' >> mailcow.conf - echo 'SKIP_UNBOUND_HEALTHCHECK=n' >> mailcow.conf - fi - elif [[ ${option} == "DISABLE_NETFILTER_ISOLATION_RULE" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n' >> mailcow.conf - echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf - echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf - fi - elif [[ ${option} == "HTTP_REDIRECT" ]]; then - if ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Redirect HTTP connections to HTTPS - y/n' >> mailcow.conf - echo 'HTTP_REDIRECT=n' >> mailcow.conf - fi - elif ! grep -q ${option} mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "${option}=n" >> mailcow.conf - fi - done -} - -migrate_solr_config_options() { - - sed -i --follow-symlinks '$a\' mailcow.conf - - if grep -q "SOLR_HEAP" mailcow.conf; then - echo "Removing SOLR_HEAP in mailcow.conf" - sed -i '/# Solr heap size in MB\b/d' mailcow.conf - sed -i '/# Solr is a prone to run\b/d' mailcow.conf - sed -i '/SOLR_HEAP\b/d' mailcow.conf - fi - - if grep -q "SKIP_SOLR" mailcow.conf; then - echo "Removing SKIP_SOLR in mailcow.conf" - sed -i '/\bSkip Solr on low-memory\b/d' mailcow.conf - sed -i '/\bSolr is disabled by default\b/d' mailcow.conf - sed -i '/\bDisable Solr or\b/d' mailcow.conf - sed -i '/\bSKIP_SOLR\b/d' mailcow.conf - fi - - if grep -q "SOLR_PORT" mailcow.conf; then - echo "Removing SOLR_PORT in mailcow.conf" - sed -i '/\bSOLR_PORT\b/d' mailcow.conf - fi - - if grep -q "FLATCURVE_EXPERIMENTAL" mailcow.conf; then - echo "Removing FLATCURVE_EXPERIMENTAL in mailcow.conf" - sed -i '/\bFLATCURVE_EXPERIMENTAL\b/d' mailcow.conf - fi - - solr_volume=$(docker volume ls -qf name=^${COMPOSE_PROJECT_NAME}_solr-vol-1) - if [[ -n $solr_volume ]]; then - echo -e "\e[34mSolr has been replaced within mailcow since 2025-01.\nThe volume $solr_volume is unused.\e[0m" - sleep 1 - if [ ! "$FORCE" ]; then - read -r -p "Remove $solr_volume? [y/N] " response - if [[ "$response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - echo -e "\e[33mRemoving $solr_volume...\e[0m" - docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m" - echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m" - else - echo -e "Not removing $solr_volume. Run \`docker volume rm $solr_volume\` manually if needed." - fi - else - echo -e "\e[33mForce removing $solr_volume...\e[0m" - docker volume rm $solr_volume || echo -e "\e[31mFailed to remove. Remove it manually!\e[0m" - echo -e "\e[32mSuccessfully removed $solr_volume!\e[0m" - fi - fi - - # Delete old fts.conf before forced switch to flatcurve to ensure update is working properly - FTS_CONF_PATH="${SCRIPT_DIR}/data/conf/dovecot/conf.d/fts.conf" - if [[ -f "$FTS_CONF_PATH" ]]; then - if grep -q "Autogenerated by mailcow" "$FTS_CONF_PATH"; then - rm -rf $FTS_CONF_PATH - fi - fi -} - -detect_major_update() { - if [ ${BRANCH} == "master" ]; then - # Array with major versions - # Add major versions here - MAJOR_VERSIONS=( - "2025-02" - "2025-03" - ) - - current_version="" - if [[ -f "${SCRIPT_DIR}/data/web/inc/app_info.inc.php" ]]; then - current_version=$(grep 'MAILCOW_GIT_VERSION' ${SCRIPT_DIR}/data/web/inc/app_info.inc.php | sed -E 's/.*MAILCOW_GIT_VERSION="([^"]+)".*/\1/') - fi - if [[ -z "$current_version" ]]; then - return 1 - fi - release_url="https://github.com/mailcow/mailcow-dockerized/releases/tag" - - updates_to_apply=() - - for version in "${MAJOR_VERSIONS[@]}"; do - if [[ "$current_version" < "$version" ]]; then - updates_to_apply+=("$version") - fi - done - - if [[ ${#updates_to_apply[@]} -gt 0 ]]; then - echo -e "\e[33m\nMAJOR UPDATES to be applied:\e[0m" - for update in "${updates_to_apply[@]}"; do - echo "$update - $release_url/$update" - done - - echo -e "\nPlease read the release notes before proceeding." - read -p "Do you want to proceed with the update? [y/n] " response - if [[ "${response}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - echo "Proceeding with the update..." - else - echo "Update canceled. Exiting." - exit 1 - fi - fi - fi -} +source _modules/scripts/core.sh +source _modules/scripts/ipv6_controller.sh +source _modules/scripts/new_options.sh +source _modules/scripts/migrate_options.sh ############## End Function Section ############## @@ -768,29 +58,11 @@ if [ "$(id -u)" -ne "0" ]; then exit 1 fi -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - # Run pre-update-hook if [ -f "${SCRIPT_DIR}/pre_update_hook.sh" ]; then bash "${SCRIPT_DIR}/pre_update_hook.sh" fi -if [[ "$(uname -r)" =~ ^4\.15\.0-60 ]]; then - echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!"; - echo "Please update to 5.x or use another distribution." - exit 1 -fi - -if [[ "$(uname -r)" =~ ^4\.4\. ]]; then - if grep -q Ubuntu <<< "$(uname -a)"; then - echo "DO NOT RUN mailcow ON THIS UBUNTU KERNEL!" - echo "Please update to linux-generic-hwe-16.04 by running \"apt-get install --install-recommends linux-generic-hwe-16.04\"" - exit 1 - fi - echo "mailcow on a 4.4.x kernel is not supported. It may or may not work, please upgrade your kernel or continue at your own risk." - read -p "Press any key to continue..." < /dev/tty -fi - # Exit on error and pipefail set -o pipefail @@ -806,22 +78,9 @@ umask 0022 unset COMPOSE_COMMAND unset DOCKER_COMPOSE_VERSION -for bin in curl docker git awk sha1sum grep cut; do - if [[ -z $(command -v ${bin}) ]]; then - echo "Cannot find ${bin}, exiting..." - exit 1; - fi -done +get_installed_tools -# Check Docker Version (need at least 24.X) -docker_version=$(docker -v | grep -oP '\d+\.\d+\.\d+' | cut -d '.' -f 1 | head -1) - -if [[ $docker_version -lt 24 ]]; then - echo -e "\e[31mCannot find Docker with a Version higher or equals 24.0.0\e[0m" - echo -e "\e[33mmailcow needs a newer Docker version to work properly... continuing on your own risk!\e[0m" - echo -e "\e[31mPlease update your Docker installation... sleeping 10s\e[0m" - sleep 10 -fi +get_docker_version export LC_ALL=C DATE=$(date +%Y-%m-%d_%H_%M_%S) @@ -925,9 +184,7 @@ done chmod 600 mailcow.conf source mailcow.conf -detect_docker_compose_command - -fix_broken_dnslist_conf +get_compose_type DOTS=${MAILCOW_HOSTNAME//[^.]}; if [ ${#DOTS} -lt 1 ]; then @@ -950,348 +207,8 @@ elif [ ${#DOTS} -eq 1 ]; then fi fi -if grep --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox grep detected, please install gnu grep, \"apk add --no-cache --upgrade grep\""; exit 1; fi -# This will also cover sort -if cp --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox cp detected, please install coreutils, \"apk add --no-cache --upgrade coreutils\""; exit 1; fi -if sed --help 2>&1 | head -n 1 | grep -q -i "busybox"; then echo "BusyBox sed detected, please install gnu sed, \"apk add --no-cache --upgrade sed\""; exit 1; fi - -CONFIG_ARRAY=( - "SKIP_LETS_ENCRYPT" - "SKIP_SOGO" - "USE_WATCHDOG" - "WATCHDOG_NOTIFY_EMAIL" - "WATCHDOG_NOTIFY_WEBHOOK" - "WATCHDOG_NOTIFY_WEBHOOK_BODY" - "WATCHDOG_NOTIFY_BAN" - "WATCHDOG_NOTIFY_START" - "WATCHDOG_EXTERNAL_CHECKS" - "WATCHDOG_SUBJECT" - "SKIP_CLAMD" - "SKIP_IP_CHECK" - "ADDITIONAL_SAN" - "AUTODISCOVER_SAN" - "DOVEADM_PORT" - "IPV4_NETWORK" - "IPV6_NETWORK" - "LOG_LINES" - "SNAT_TO_SOURCE" - "SNAT6_TO_SOURCE" - "COMPOSE_PROJECT_NAME" - "DOCKER_COMPOSE_VERSION" - "SQL_PORT" - "API_KEY" - "API_KEY_READ_ONLY" - "API_ALLOW_FROM" - "MAILDIR_GC_TIME" - "MAILDIR_SUB" - "ACL_ANYONE" - "ENABLE_SSL_SNI" - "ALLOW_ADMIN_EMAIL_LOGIN" - "SKIP_HTTP_VERIFICATION" - "SOGO_EXPIRE_SESSION" - "REDIS_PORT" - "DOVECOT_MASTER_USER" - "DOVECOT_MASTER_PASS" - "MAILCOW_PASS_SCHEME" - "ADDITIONAL_SERVER_NAMES" - "ACME_CONTACT" - "WATCHDOG_VERBOSE" - "WEBAUTHN_ONLY_TRUSTED_VENDORS" - "SPAMHAUS_DQS_KEY" - "SKIP_UNBOUND_HEALTHCHECK" - "DISABLE_NETFILTER_ISOLATION_RULE" - "REDISPASS" -) - detect_bad_asn -sed -i --follow-symlinks '$a\' mailcow.conf -for option in "${CONFIG_ARRAY[@]}"; do - if [[ ${option} == "ADDITIONAL_SAN" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "${option}=" >> mailcow.conf - fi - elif [[ "${option}" == "COMPOSE_PROJECT_NAME" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "COMPOSE_PROJECT_NAME=mailcowdockerized" >> mailcow.conf - fi - elif [[ "${option}" == "DOCKER_COMPOSE_VERSION" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "# Used Docker Compose version" >> mailcow.conf - echo "# Switch here between native (compose plugin) and standalone" >> mailcow.conf - echo "# For more informations take a look at the mailcow docs regarding the configuration options." >> mailcow.conf - echo "# Normally this should be untouched but if you decided to use either of those you can switch it manually here." >> mailcow.conf - echo "# Please be aware that at least one of those variants should be installed on your maschine or mailcow will fail." >> mailcow.conf - echo "" >> mailcow.conf - echo "DOCKER_COMPOSE_VERSION=${DOCKER_COMPOSE_VERSION}" >> mailcow.conf - fi - elif [[ "${option}" == "DOVEADM_PORT" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "DOVEADM_PORT=127.0.0.1:19991" >> mailcow.conf - fi - elif [[ "${option}" == "WATCHDOG_NOTIFY_EMAIL" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "WATCHDOG_NOTIFY_EMAIL=" >> mailcow.conf - fi - elif [[ "${option}" == "LOG_LINES" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Max log lines per service to keep in Redis logs' >> mailcow.conf - echo "LOG_LINES=9999" >> mailcow.conf - fi - elif [[ "${option}" == "IPV4_NETWORK" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Internal IPv4 /24 subnet, format n.n.n. (expands to n.n.n.0/24)' >> mailcow.conf - echo "IPV4_NETWORK=172.22.1" >> mailcow.conf - fi - elif [[ "${option}" == "IPV6_NETWORK" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Internal IPv6 subnet in fc00::/7' >> mailcow.conf - echo "IPV6_NETWORK=fd4d:6169:6c63:6f77::/64" >> mailcow.conf - fi - elif [[ "${option}" == "SQL_PORT" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Bind SQL to 127.0.0.1 on port 13306' >> mailcow.conf - echo "SQL_PORT=127.0.0.1:13306" >> mailcow.conf - fi - elif [[ "${option}" == "API_KEY" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Create or override API key for web UI' >> mailcow.conf - echo "#API_KEY=" >> mailcow.conf - fi - elif [[ "${option}" == "API_KEY_READ_ONLY" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Create or override read-only API key for web UI' >> mailcow.conf - echo "#API_KEY_READ_ONLY=" >> mailcow.conf - fi - elif [[ "${option}" == "API_ALLOW_FROM" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Must be set for API_KEY to be active' >> mailcow.conf - echo '# IPs only, no networks (networks can be set via UI)' >> mailcow.conf - echo "#API_ALLOW_FROM=" >> mailcow.conf - fi - elif [[ "${option}" == "SNAT_TO_SOURCE" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Use this IPv4 for outgoing connections (SNAT)' >> mailcow.conf - echo "#SNAT_TO_SOURCE=" >> mailcow.conf - fi - elif [[ "${option}" == "SNAT6_TO_SOURCE" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Use this IPv6 for outgoing connections (SNAT)' >> mailcow.conf - echo "#SNAT6_TO_SOURCE=" >> mailcow.conf - fi - elif [[ "${option}" == "MAILDIR_GC_TIME" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Garbage collector cleanup' >> mailcow.conf - echo '# Deleted domains and mailboxes are moved to /var/vmail/_garbage/timestamp_sanitizedstring' >> mailcow.conf - echo '# How long should objects remain in the garbage until they are being deleted? (value in minutes)' >> mailcow.conf - echo '# Check interval is hourly' >> mailcow.conf - echo 'MAILDIR_GC_TIME=1440' >> mailcow.conf - fi - elif [[ "${option}" == "ACL_ANYONE" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Set this to "allow" to enable the anyone pseudo user. Disabled by default.' >> mailcow.conf - echo '# When enabled, ACL can be created, that apply to "All authenticated users"' >> mailcow.conf - echo '# This should probably only be activated on mail hosts, that are used exclusivly by one organisation.' >> mailcow.conf - echo '# Otherwise a user might share data with too many other users.' >> mailcow.conf - echo 'ACL_ANYONE=disallow' >> mailcow.conf - fi - elif [[ "${option}" == "ENABLE_SSL_SNI" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Create seperate certificates for all domains - y/n' >> mailcow.conf - echo '# this will allow adding more than 100 domains, but some email clients will not be able to connect with alternative hostnames' >> mailcow.conf - echo '# see https://wiki.dovecot.org/SSL/SNIClientSupport' >> mailcow.conf - echo "ENABLE_SSL_SNI=n" >> mailcow.conf - fi - elif [[ "${option}" == "SKIP_SOGO" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Skip SOGo: Will disable SOGo integration and therefore webmail, DAV protocols and ActiveSync support (experimental, unsupported, not fully implemented) - y/n' >> mailcow.conf - echo "SKIP_SOGO=n" >> mailcow.conf - fi - elif [[ "${option}" == "MAILDIR_SUB" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# MAILDIR_SUB defines a path in a users virtual home to keep the maildir in. Leave empty for updated setups.' >> mailcow.conf - echo "#MAILDIR_SUB=Maildir" >> mailcow.conf - echo "MAILDIR_SUB=" >> mailcow.conf - fi - elif [[ "${option}" == "WATCHDOG_NOTIFY_WEBHOOK" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Send notifications to a webhook URL that receives a POST request with the content type "application/json".' >> mailcow.conf - echo '# You can use this to send notifications to services like Discord, Slack and others.' >> mailcow.conf - echo '#WATCHDOG_NOTIFY_WEBHOOK=https://discord.com/api/webhooks/XXXXXXXXXXXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' >> mailcow.conf - fi - elif [[ "${option}" == "WATCHDOG_NOTIFY_WEBHOOK_BODY" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# JSON body included in the webhook POST request. Needs to be in single quotes.' >> mailcow.conf - echo '# Following variables are available: SUBJECT, BODY' >> mailcow.conf - WEBHOOK_BODY='{"username": "mailcow Watchdog", "content": "**${SUBJECT}**\n${BODY}"}' - echo "#WATCHDOG_NOTIFY_WEBHOOK_BODY='${WEBHOOK_BODY}'" >> mailcow.conf - fi - elif [[ "${option}" == "WATCHDOG_NOTIFY_BAN" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Notify about banned IP. Includes whois lookup.' >> mailcow.conf - echo "WATCHDOG_NOTIFY_BAN=y" >> mailcow.conf - fi - elif [[ "${option}" == "WATCHDOG_NOTIFY_START" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Send a notification when the watchdog is started.' >> mailcow.conf - echo "WATCHDOG_NOTIFY_START=y" >> mailcow.conf - fi - elif [[ "${option}" == "WATCHDOG_SUBJECT" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Subject for watchdog mails. Defaults to "Watchdog ALERT" followed by the error message.' >> mailcow.conf - echo "#WATCHDOG_SUBJECT=" >> mailcow.conf - fi - elif [[ "${option}" == "WATCHDOG_EXTERNAL_CHECKS" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Checks if mailcow is an open relay. Requires a SAL. More checks will follow.' >> mailcow.conf - echo '# No data is collected. Opt-in and anonymous.' >> mailcow.conf - echo '# Will only work with unmodified mailcow setups.' >> mailcow.conf - echo "WATCHDOG_EXTERNAL_CHECKS=n" >> mailcow.conf - fi - elif [[ "${option}" == "SOGO_EXPIRE_SESSION" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# SOGo session timeout in minutes' >> mailcow.conf - echo "SOGO_EXPIRE_SESSION=480" >> mailcow.conf - fi - elif [[ "${option}" == "REDIS_PORT" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "REDIS_PORT=127.0.0.1:7654" >> mailcow.conf - fi - elif [[ "${option}" == "DOVECOT_MASTER_USER" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# DOVECOT_MASTER_USER and _PASS must _both_ be provided. No special chars.' >> mailcow.conf - echo '# Empty by default to auto-generate master user and password on start.' >> mailcow.conf - echo '# User expands to DOVECOT_MASTER_USER@mailcow.local' >> mailcow.conf - echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf - echo "DOVECOT_MASTER_USER=" >> mailcow.conf - fi - elif [[ "${option}" == "DOVECOT_MASTER_PASS" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# LEAVE EMPTY IF UNSURE' >> mailcow.conf - echo "DOVECOT_MASTER_PASS=" >> mailcow.conf - fi - elif [[ "${option}" == "MAILCOW_PASS_SCHEME" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Password hash algorithm' >> mailcow.conf - echo '# Only certain password hash algorithm are supported. For a fully list of supported schemes,' >> mailcow.conf - echo '# see https://docs.mailcow.email/models/model-passwd/' >> mailcow.conf - echo "MAILCOW_PASS_SCHEME=BLF-CRYPT" >> mailcow.conf - fi - elif [[ "${option}" == "ADDITIONAL_SERVER_NAMES" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Additional server names for mailcow UI' >> mailcow.conf - echo '#' >> mailcow.conf - echo '# Specify alternative addresses for the mailcow UI to respond to' >> mailcow.conf - echo '# This is useful when you set mail.* as ADDITIONAL_SAN and want to make sure mail.maildomain.com will always point to the mailcow UI.' >> mailcow.conf - echo '# If the server name does not match a known site, Nginx decides by best-guess and may redirect users to the wrong web root.' >> mailcow.conf - echo '# You can understand this as server_name directive in Nginx.' >> mailcow.conf - echo '# Comma separated list without spaces! Example: ADDITIONAL_SERVER_NAMES=a.b.c,d.e.f' >> mailcow.conf - echo 'ADDITIONAL_SERVER_NAMES=' >> mailcow.conf - fi - - elif [[ "${option}" == "AUTODISCOVER_SAN" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Obtain certificates for autodiscover.* and autoconfig.* domains.' >> mailcow.conf - echo '# This can be useful to switch off in case you are in a scenario where a reverse proxy already handles those.' >> mailcow.conf - echo '# There are mixed scenarios where ports 80,443 are occupied and you do not want to share certs' >> mailcow.conf - echo '# between services. So acme-mailcow obtains for maildomains and all web-things get handled' >> mailcow.conf - echo '# in the reverse proxy.' >> mailcow.conf - echo 'AUTODISCOVER_SAN=y' >> mailcow.conf - fi - - elif [[ "${option}" == "ACME_CONTACT" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Lets Encrypt registration contact information' >> mailcow.conf - echo '# Optional: Leave empty for none' >> mailcow.conf - echo '# This value is only used on first order!' >> mailcow.conf - echo '# Setting it at a later point will require the following steps:' >> mailcow.conf - echo '# https://docs.mailcow.email/troubleshooting/debug-reset_tls/' >> mailcow.conf - echo 'ACME_CONTACT=' >> mailcow.conf - fi - elif [[ "${option}" == "WEBAUTHN_ONLY_TRUSTED_VENDORS" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "# WebAuthn device manufacturer verification" >> mailcow.conf - echo '# After setting WEBAUTHN_ONLY_TRUSTED_VENDORS=y only devices from trusted manufacturers are allowed' >> mailcow.conf - echo '# root certificates can be placed for validation under mailcow-dockerized/data/web/inc/lib/WebAuthn/rootCertificates' >> mailcow.conf - echo 'WEBAUTHN_ONLY_TRUSTED_VENDORS=n' >> mailcow.conf - fi - elif [[ "${option}" == "SPAMHAUS_DQS_KEY" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "# Spamhaus Data Query Service Key" >> mailcow.conf - echo '# Optional: Leave empty for none' >> mailcow.conf - echo '# Enter your key here if you are using a blocked ASN (OVH, AWS, Cloudflare e.g) for the unregistered Spamhaus Blocklist.' >> mailcow.conf - echo '# If empty, it will completely disable Spamhaus blocklists if it detects that you are running on a server using a blocked AS.' >> mailcow.conf - echo '# Otherwise it will work as usual.' >> mailcow.conf - echo 'SPAMHAUS_DQS_KEY=' >> mailcow.conf - fi - elif [[ "${option}" == "WATCHDOG_VERBOSE" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Enable watchdog verbose logging' >> mailcow.conf - echo 'WATCHDOG_VERBOSE=n' >> mailcow.conf - fi - elif [[ "${option}" == "SKIP_UNBOUND_HEALTHCHECK" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Skip Unbound (DNS Resolver) Healthchecks (NOT Recommended!) - y/n' >> mailcow.conf - echo 'SKIP_UNBOUND_HEALTHCHECK=n' >> mailcow.conf - fi - elif [[ "${option}" == "DISABLE_NETFILTER_ISOLATION_RULE" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo '# Prevent netfilter from setting an iptables/nftables rule to isolate the mailcow docker network - y/n' >> mailcow.conf - echo '# CAUTION: Disabling this may expose container ports to other neighbors on the same subnet, even if the ports are bound to localhost' >> mailcow.conf - echo 'DISABLE_NETFILTER_ISOLATION_RULE=n' >> mailcow.conf - fi - elif [[ "${option}" == "REDISPASS" ]]; then - if ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo -e '\n# ------------------------------' >> mailcow.conf - echo '# REDIS configuration' >> mailcow.conf - echo -e '# ------------------------------\n' >> mailcow.conf - echo "REDISPASS=$(LC_ALL=C /dev/null | head -c 28)" >> mailcow.conf - fi - elif ! grep -q "${option}" mailcow.conf; then - echo "Adding new option \"${option}\" to mailcow.conf" - echo "${option}=n" >> mailcow.conf - fi -done - if [[ ("${SKIP_PING_CHECK}" == "y") ]]; then echo -e "\e[32mSkipping Ping Check...\e[0m" @@ -1410,15 +327,15 @@ elif [ "$NEW_BRANCH" == "legacy" ] && [ "$CURRENT_BRANCH" != "legacy" ]; then fi if [ ! "$DEV" ]; then + EXIT_COUNT=0 echo -e "\e[32mChecking for newer update script...\e[0m" SHA1_1="$(sha1sum update.sh)" - git fetch origin #${BRANCH} - git checkout "origin/${BRANCH}" update.sh - SHA1_2=$(sha1sum update.sh) + git fetch origin + git checkout "origin/${BRANCH}" -- update.sh + SHA1_2="$(sha1sum update.sh)" if [[ "${SHA1_1}" != "${SHA1_2}" ]]; then - echo "update.sh changed, please run this script again, exiting." chmod +x update.sh - exit 2 + EXIT_COUNT+=1 fi fi @@ -1429,11 +346,8 @@ if [ ! "$FORCE" ]; then exit 0 fi detect_major_update - migrate_docker_nat fi -remove_obsolete_nginx_ports - echo -e "\e[32mValidating docker-compose stack configuration...\e[0m" sed -i 's/HTTPS_BIND:-:/HTTPS_BIND:-/g' docker-compose.yml sed -i 's/HTTP_BIND:-:/HTTP_BIND:-/g' docker-compose.yml @@ -1471,27 +385,28 @@ for container in "${MAILCOW_CONTAINERS[@]}"; do docker rm -f "$container" 2> /dev/null done +configure_ipv6 + [[ -f data/conf/nginx/ZZZ-ejabberd.conf ]] && rm data/conf/nginx/ZZZ-ejabberd.conf -migrate_solr_config_options +migrate_config_options adapt_new_options -# Silently fixing remote url from andryyy to mailcow -# git remote set-url origin https://github.com/mailcow/mailcow-dockerized - -DEFAULT_REPO="https://github.com/mailcow/mailcow-dockerized" -CURRENT_REPO=$(git config --get remote.origin.url) -if [ "$CURRENT_REPO" != "$DEFAULT_REPO" ]; then - echo "The Repository currently used is not the default Mailcow Repository." - echo "Currently Repository: $CURRENT_REPO" - echo "Default Repository: $DEFAULT_REPO" - if [ ! "$FORCE" ]; then - read -r -p "Should it be changed back to default? [y/N] " repo_response - if [[ "$repo_response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then - git remote set-url origin $DEFAULT_REPO +if [ ! "$DEV" ]; then + DEFAULT_REPO="https://github.com/mailcow/mailcow-dockerized" + CURRENT_REPO=$(git config --get remote.origin.url) + if [ "$CURRENT_REPO" != "$DEFAULT_REPO" ]; then + echo "The Repository currently used is not the default mailcow Repository." + echo "Currently Repository: $CURRENT_REPO" + echo "Default Repository: $DEFAULT_REPO" + if [ ! "$FORCE" ]; then + read -r -p "Should it be changed back to default? [y/N] " repo_response + if [[ "$repo_response" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + git remote set-url origin $DEFAULT_REPO + fi + else + echo "Running in forced mode... setting Repo to default!" + git remote set-url origin $DEFAULT_REPO fi - else - echo "Running in forced mode... setting Repo to default!" - git remote set-url origin $DEFAULT_REPO fi fi @@ -1525,7 +440,7 @@ if [ ! "$DEV" ]; then echo "Run $COMPOSE_COMMAND up -d to restart your stack without updates or try again after fixing the mentioned errors." exit 1 fi -elif [ "$DEV" ]; then +else echo -e "\e[33mDEVELOPER MODE: Not creating a git diff and commiting it to prevent development stuff within a backup diff...\e[0m" fi diff --git a/nextcloud/docker-compose.yml b/nextcloud/docker-compose.yml index 921fccd..dfacf7a 100644 --- a/nextcloud/docker-compose.yml +++ b/nextcloud/docker-compose.yml @@ -1,7 +1,7 @@ # Nextcloud-Stack services: nextcloud: - image: nextcloud:31.0-fpm + image: nextcloud:32-fpm container_name: nextcloud depends_on: - nextcloud-redis @@ -10,6 +10,7 @@ services: - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-admin} - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD:-admin} - NEXTCLOUD_DEFAULT_PHONE_REGION=DE + - NEXTCLOUD_DEFAULT_LANGUAGE=${NEXTCLOUD_DEFAULT_LANGUAGE:-de} - ONLYOFFICE_JWT_SECRET=${ONLYOFFICE_JWT_SECRET} - POSTGRES_DB=${NEXTCLOUD_DB_NAME} - POSTGRES_HOST=${NEXTCLOUD_DB_HOST} @@ -18,11 +19,17 @@ services: - OVERWRITEPROTOCOL=https - OVERWRITEHOST=${NEXTCLOUD_DOMAIN} - REDIS_HOST=nextcloud-redis + - TRUSTED_PROXIES=172.22.0.0/16 172.19.0.0/16 + - COLLABORA_DOMAIN=${COLLABORA_DOMAIN} + - TALK_DOMAIN=${TALK_DOMAIN} + - TALK_TURN_SECRET=${TALK_TURN_SECRET} + - TALK_SIGNALING_SECRET=${TALK_SIGNALING_SECRET} labels: - "traefik.enable=false" volumes: - nextcloud-data:/var/www/html - ./hooks/post-installation:/docker-entrypoint-hooks.d/post-installation + - ./php/opcache.ini:/usr/local/etc/php/conf.d/opcache-recommended.ini:ro expose: - 80 - 9000 @@ -61,6 +68,65 @@ services: - nextcloud restart: unless-stopped + collabora: + image: collabora/code:latest + container_name: nextcloud-collabora + environment: + - domain=${NEXTCLOUD_DOMAIN} + - aliasgroup1=https://${NEXTCLOUD_DOMAIN}:443 + - username=${COLLABORA_USERNAME} + - password=${COLLABORA_PASSWORD} + - extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:welcome.enable=false --o:logging.level=warning + - dictionaries=de_DE en_GB en_US + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + - "traefik.http.routers.collabora.rule=Host(`${COLLABORA_DOMAIN}`)" + - "traefik.http.routers.collabora.entrypoints=web,websecure" + - "traefik.http.routers.collabora.middlewares=https-redirect" + - "traefik.http.routers.collabora.tls=true" + - "traefik.http.routers.collabora.tls.certresolver=le" + - "traefik.http.services.collabora.loadbalancer.server.port=9980" + - "traefik.http.services.collabora.loadbalancer.server.scheme=http" + networks: + - nextcloud + - traefik + restart: unless-stopped + cap_add: + - MKNOD + - SYS_ADMIN + security_opt: + - apparmor:unconfined + + nc-talk: + container_name: nc-talk + image: ghcr.io/nextcloud-releases/aio-talk:latest + init: true + ports: + - "3478:3478/tcp" + - "3478:3478/udp" + environment: + - NC_DOMAIN=${NEXTCLOUD_DOMAIN} + - TALK_HOST=${TALK_DOMAIN} + - TURN_SECRET=${TALK_TURN_SECRET} + - SIGNALING_SECRET=${TALK_SIGNALING_SECRET} + - INTERNAL_SECRET=${TALK_INTERNAL_SECRET} + - TZ=${TZ:-Europe/Berlin} + - TALK_PORT=3478 + labels: + - "traefik.enable=true" + - "traefik.docker.network=traefik" + - "traefik.http.routers.nc-talk.rule=Host(`${TALK_DOMAIN}`)" + - "traefik.http.routers.nc-talk.entrypoints=websecure" + - "traefik.http.routers.nc-talk.middlewares=https-redirect" + - "traefik.http.routers.nc-talk.tls=true" + - "traefik.http.routers.nc-talk.tls.certresolver=le" + - "traefik.http.services.nc-talk.loadbalancer.server.port=8081" + networks: + - nextcloud + - traefik + restart: unless-stopped + volumes: nextcloud-data: name: nextcloud-data diff --git a/nextcloud/hooks/post-installation/install-nextcloud-office.sh b/nextcloud/hooks/post-installation/install-nextcloud-office.sh new file mode 100755 index 0000000..b6d3d8a --- /dev/null +++ b/nextcloud/hooks/post-installation/install-nextcloud-office.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +echo "Installing Nextcloud Office (richdocuments) app..." +php /var/www/html/occ app:install richdocuments +echo "Nextcloud Office installed successfully!" + +echo "Enabling Nextcloud Office..." +php /var/www/html/occ app:enable richdocuments +echo "Nextcloud Office enabled successfully!" + +echo "Configuring Nextcloud Office with Collabora Online..." + +# Set the WOPI URL (Collabora server URL) +if [ -n "${COLLABORA_DOMAIN}" ]; then + echo "Setting WOPI URL to https://${COLLABORA_DOMAIN}" + php /var/www/html/occ --no-warnings config:app:set richdocuments wopi_url --value="https://${COLLABORA_DOMAIN}" +else + echo "Warning: COLLABORA_DOMAIN not set, skipping WOPI URL configuration" +fi + +# Disable certificate verification (set to 'yes' only for development/self-signed certs) +php /var/www/html/occ --no-warnings config:app:set richdocuments disable_certificate_verification --value="no" + +# Allow list for WOPI requests (optional, for additional security) +if [ -n "${COLLABORA_DOMAIN}" ]; then + php /var/www/html/occ --no-warnings config:app:set richdocuments wopi_allowlist --value="${COLLABORA_DOMAIN}" +fi + +# Enable the built-in CODE server option (if using built-in Collabora) +# Uncomment if you want to use the built-in CODE server instead of external Collabora +# php /var/www/html/occ --no-warnings config:app:set richdocuments use_built_in_code_server --value="yes" + +echo "Nextcloud Office configuration completed!" +echo "Collabora Online should now be available at: https://${COLLABORA_DOMAIN}" diff --git a/nextcloud/hooks/post-installation/install-talk.sh b/nextcloud/hooks/post-installation/install-talk.sh new file mode 100755 index 0000000..9d560ec --- /dev/null +++ b/nextcloud/hooks/post-installation/install-talk.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e + +echo "Installing Nextcloud Talk app..." +php /var/www/html/occ app:install spreed +echo "Nextcloud Talk installed successfully!" + +echo "Enabling Nextcloud Talk..." +php /var/www/html/occ app:enable spreed +echo "Nextcloud Talk enabled successfully!" + +echo "Configuring Nextcloud Talk HPB..." + +if [ -z "${TALK_DOMAIN}" ]; then + echo "Warning: TALK_DOMAIN not set, skipping Talk HPB configuration." + exit 0 +fi + +# STUN server +php /var/www/html/occ --no-warnings talk:stun:add "${TALK_DOMAIN}:3478" + +# TURN server +if [ -n "${TALK_TURN_SECRET}" ]; then + php /var/www/html/occ --no-warnings talk:turn:add turn "${TALK_DOMAIN}:3478" udp,tcp --secret="${TALK_TURN_SECRET}" +else + echo "Warning: TALK_TURN_SECRET not set, skipping TURN configuration." +fi + +# High Performance Backend (signaling server) +if [ -n "${TALK_SIGNALING_SECRET}" ]; then + php /var/www/html/occ --no-warnings talk:signaling:add "https://${TALK_DOMAIN}" "${TALK_SIGNALING_SECRET}" +else + echo "Warning: TALK_SIGNALING_SECRET not set, skipping HPB signaling configuration." +fi + +echo "Nextcloud Talk configuration completed!" +echo "STUN/TURN/HPB configured for: ${TALK_DOMAIN}" diff --git a/nextcloud/nextcloud-maintenance.sh b/nextcloud/nextcloud-maintenance.sh new file mode 100755 index 0000000..8517bbc --- /dev/null +++ b/nextcloud/nextcloud-maintenance.sh @@ -0,0 +1,226 @@ +#!/bin/bash + +# Nextcloud Maintenance Script. +# This script performs maintenance tasks for Nextcloud. + +set -e + +# Colors for output. +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color. + +CONTAINER_NAME="nextcloud" + +# Function to print colored messages. +printMessage() { + local color=$1 + local message=$2 + echo -e "${color}${message}${NC}" +} + +# Function to run occ command. +runOcc() { + docker exec -u www-data "$CONTAINER_NAME" php occ "$@" +} + +# Load environment variables. +loadEnv() { + if [ -f "./nextcloud/.env" ]; then + source ./nextcloud/.env + printMessage "$GREEN" "Loaded Nextcloud environment variables." + else + printMessage "$RED" "Nextcloud .env file not found!" + exit 1 + fi + + if [ -f "./core/.env" ]; then + source ./core/.env + printMessage "$GREEN" "Loaded core environment variables." + else + printMessage "$RED" "Core .env file not found!" + exit 1 + fi +} + +# Function to check if container is running. +checkContainer() { + if ! docker ps | grep -q "$CONTAINER_NAME"; then + printMessage "$RED" "Error: Container $CONTAINER_NAME is not running!" + exit 1 + fi +} + +# Function to fix database collation version mismatch. +fix_database_collation() { + printMessage "$YELLOW" "Checking and fixing database collation version..." + + if docker exec postgres psql -U "${POSTGRES_USER}" -d "${NEXTCLOUD_DB_NAME}" -c "ALTER DATABASE ${NEXTCLOUD_DB_NAME} REFRESH COLLATION VERSION;" 2>&1 | grep -q "ALTER DATABASE"; then + printMessage "$GREEN" "Database collation version updated successfully." + return 0 + else + printMessage "$YELLOW" "Database collation check completed (may already be up to date)." + return 0 + fi +} + +# Function to check Nextcloud status. +check_status() { + printMessage "$YELLOW" "Checking Nextcloud status..." + runOcc status + return 0 +} + +# Function to scan files. +scan_files() { + if [ -z "$1" ]; then + printMessage "$YELLOW" "Scanning all files..." + runOcc files:scan --all + else + printMessage "$YELLOW" "Scanning files for user: $1..." + runOcc files:scan "$1" + fi + return 0 +} + +# Function to update Nextcloud. +update_nextcloud() { + printMessage "$YELLOW" "Running database upgrade..." + runOcc upgrade + return 0 +} + +# Function to optimize database. +optimize_database() { + printMessage "$YELLOW" "Optimizing database..." + runOcc db:add-missing-indices + runOcc db:add-missing-columns + runOcc db:add-missing-primary-keys + return 0 +} + +# Function to run maintenance repair. +run_repair() { + printMessage "$YELLOW" "Running maintenance repair..." + runOcc maintenance:repair + return 0 +} + +# Function to run comprehensive post-update maintenance. +post_update_maintenance() { + checkContainer + + printMessage "$GREEN" "=========================================" + printMessage "$GREEN" "Nextcloud Post-Update Maintenance" + printMessage "$GREEN" "=========================================" + echo "" + + printMessage "$YELLOW" "[1/10] Enabling maintenance mode..." + runOcc maintenance:mode --on + + printMessage "$YELLOW" "[2/10] Running database upgrade..." + runOcc upgrade + + printMessage "$YELLOW" "[3/10] Adding missing database indices..." + runOcc db:add-missing-indices + + printMessage "$YELLOW" "[4/10] Adding missing database columns..." + runOcc db:add-missing-columns + + printMessage "$YELLOW" "[5/10] Adding missing primary keys..." + runOcc db:add-missing-primary-keys + + printMessage "$YELLOW" "[6/10] Converting filecache to big int (if needed)..." + runOcc db:convert-filecache-bigint --no-interaction || printMessage "$YELLOW" "Already converted or not needed." + + printMessage "$YELLOW" "[7/10] Updating .htaccess and configuration files..." + runOcc maintenance:update:htaccess + + printMessage "$YELLOW" "[8/10] Updating theme..." + runOcc maintenance:theme:update + + printMessage "$YELLOW" "[9/10] Running repair steps..." + runOcc maintenance:repair + + printMessage "$YELLOW" "[10/10] Disabling maintenance mode..." + runOcc maintenance:mode --off + + echo "" + printMessage "$GREEN" "=========================================" + printMessage "$GREEN" "Maintenance completed successfully!" + printMessage "$GREEN" "=========================================" + echo "" + + printMessage "$YELLOW" "System Status:" + runOcc status + + echo "" + printMessage "$GREEN" "You may want to run background jobs manually:" + printMessage "$YELLOW" " docker exec -u www-data $CONTAINER_NAME php occ background:job:execute" +} + +# Main execution. +printMessage "$YELLOW" "Running Nextcloud maintenance tasks..." + +case "${1:-all}" in + collation) + loadEnv + checkContainer + fix_database_collation + ;; + status) + checkContainer + check_status + ;; + scan) + checkContainer + scan_files "$2" + ;; + update) + checkContainer + update_nextcloud + ;; + optimize) + checkContainer + optimize_database + ;; + repair) + checkContainer + run_repair + ;; + post-update) + loadEnv + post_update_maintenance + ;; + all) + loadEnv + checkContainer + fix_database_collation + echo "" + check_status + echo "" + optimize_database + echo "" + run_repair + echo "" + printMessage "$YELLOW" "Disabling maintenance mode..." + runOcc maintenance:mode --off + echo "" + printMessage "$GREEN" "All maintenance tasks completed." + ;; + *) + echo "Usage: $0 [collation|status|scan|update|optimize|repair|post-update|all]" + echo "" + echo "Commands:" + echo " collation - Fix database collation version mismatch" + echo " status - Check Nextcloud status" + echo " scan - Scan files (optionally specify username)" + echo " update - Update Nextcloud database" + echo " optimize - Optimize database indices and columns" + echo " repair - Run maintenance repair" + echo " post-update - Run comprehensive post-update maintenance workflow" + echo " all - Run all basic maintenance tasks (default)" + exit 1 + ;; +esac diff --git a/nextcloud/php/opcache.ini b/nextcloud/php/opcache.ini new file mode 100644 index 0000000..180f534 --- /dev/null +++ b/nextcloud/php/opcache.ini @@ -0,0 +1,6 @@ +opcache.enable=1 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=10000 +opcache.memory_consumption=256 +opcache.save_comments=1 +opcache.revalidate_freq=60 diff --git a/nextcloud/reverse-proxy/nginx.conf b/nextcloud/reverse-proxy/nginx.conf index f23a318..e6c9c80 100644 --- a/nextcloud/reverse-proxy/nginx.conf +++ b/nextcloud/reverse-proxy/nginx.conf @@ -82,15 +82,15 @@ http { # The rules in this block are an adaptation of the rules # in `.htaccess` that concern `/.well-known`. - location = /.well-known/carddav { return 301 /remote.php/dav; } - location = /.well-known/caldav { return 301 /remote.php/dav; } + location = /.well-known/carddav { return 301 https://$host/remote.php/dav; } + location = /.well-known/caldav { return 301 https://$host/remote.php/dav; } location /.well-known/acme-challenge { try_files $uri $uri/ =404; } location /.well-known/pki-validation { try_files $uri $uri/ =404; } # Let Nextcloud's API for `/.well-known` URIs handle all other # requests by passing them to the front-end controller. - return 301 /index.php$request_uri; + return 301 https://$host/index.php$request_uri; } location = /robots.txt { @@ -138,6 +138,10 @@ http { fastcgi_param PATH_INFO $fastcgi_path_info; fastcgi_param HTTPS on; fastcgi_param modHeadersAvailable true; #Avoid sending the security headers twice + fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for; + fastcgi_param HTTP_X_FORWARDED_PROTO $scheme; + fastcgi_param HTTP_X_FORWARDED_HOST $host; + fastcgi_param REMOTE_ADDR $remote_addr; fastcgi_pass backend; fastcgi_intercept_errors on; } diff --git a/onlyoffice/.gitignore b/onlyoffice/.gitignore deleted file mode 100644 index b8865ed..0000000 --- a/onlyoffice/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.env -docker-compose.override.yml diff --git a/onlyoffice/.travis.yml b/onlyoffice/.travis.yml deleted file mode 100644 index 2e37ca6..0000000 --- a/onlyoffice/.travis.yml +++ /dev/null @@ -1,131 +0,0 @@ -language: generic - -dist: trusty - -env: - # community edition - - config: standalone.yml - - # integration edition - - config: standalone.yml - PRODUCT_NAME: documentserver-ie - - - # certificates (default tls if onlyoffice not exists) - - config: certs.yml - ssl: true - - # certificates (default onlyoffice if exists) - - config: certs.yml - ssl: true - private_key: onlyoffice.key - certificate_request: onlyoffice.csr - certificate: onlyoffice.crt - - # custom certificates - - config: certs-customized.yml - ssl: true - private_key: mycert.key - certificate_request: mycert.csr - certificate: mycert.crt - SSL_CERTIFICATE_PATH: /var/www/onlyoffice/Data/certs/mycert.crt - SSL_KEY_PATH: /var/www/onlyoffice/Data/certs/mycert.key - - - # postgresql 16 - - config: postgres.yml - POSTGRES_VERSION: 16 - - # postgresql 15 - - config: postgres.yml - POSTGRES_VERSION: 15 - - # postgresql 14 - - config: postgres.yml - POSTGRES_VERSION: 14 - - # postgresql 13 - - config: postgres.yml - POSTGRES_VERSION: 13 - - # postgresql 12 - - config: postgres.yml - - # postgresql custom values - - config: postgres.yml - DB_NAME: mydb - DB_USER: myuser - DB_PWD: password - POSTGRES_DB: mydb - POSTGRES_USER: myuser - - # postgresql deprecated variables - - config: postgres-old.yml - - - # mysql 8 - - config: mysql.yml - MYSQL_VERSION: 8 - - # mysql 5 - - config: mysql.yml - MYSQL_VERSION: 5 - - # mysql 5.7 - - config: mysql.yml - - - # mariadb 10 - - config: mariadb.yml - MARIADB_VERSION: 10 - - # mariadb 10.5 - - config: mariadb.yml - - - - config: activemq.yml - ACTIVEMQ_VERSION: latest - - # activemq 5.14.3 - - config: activemq.yml - - - # rabbitmq latest - - config: rabbitmq.yml - - # rabbitmq 3 - - config: rabbitmq.yml - RABBITMQ_VERSION: 3 - - # rabbitmq old variables - - config: rabbitmq-old.yml - - - # redis latest with community edition - - config: redis.yml - - # redis latest with integraion edition - - config: redis.yml - PRODUCT_NAME: documentserver-ie - - # redis 6 - - config: redis.yml - REDIS_VERSION: 6 - - # redis 5 - - config: redis.yml - REDIS_VERSION: 5 - - - # graphite - - config: graphite.yml - -services: - - docker - -script: - # Go to tests dir - - cd ${PWD}/tests - - # Run test. - - ./test.sh diff --git a/onlyoffice/Dockerfile b/onlyoffice/Dockerfile deleted file mode 100644 index 63f140c..0000000 --- a/onlyoffice/Dockerfile +++ /dev/null @@ -1,138 +0,0 @@ -ARG BASE_VERSION=24.04 - -ARG BASE_IMAGE=ubuntu:$BASE_VERSION - -FROM ${BASE_IMAGE} AS documentserver -LABEL maintainer Ascensio System SIA - -ARG BASE_VERSION -ARG PG_VERSION=16 -ARG PACKAGE_SUFFIX=t64 - -ENV OC_RELEASE_NUM=21 -ENV OC_RU_VER=12 -ENV OC_RU_REVISION_VER=0 -ENV OC_RESERVED_NUM=0 -ENV OC_RU_DATE=0 -ENV OC_PATH=${OC_RELEASE_NUM}${OC_RU_VER}000 -ENV OC_FILE_SUFFIX=${OC_RELEASE_NUM}.${OC_RU_VER}.${OC_RU_REVISION_VER}.${OC_RESERVED_NUM}.${OC_RU_DATE}${OC_FILE_SUFFIX}dbru -ENV OC_VER_DIR=${OC_RELEASE_NUM}_${OC_RU_VER} -ENV OC_DOWNLOAD_URL=https://download.oracle.com/otn_software/linux/instantclient/${OC_PATH} - -ENV LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 DEBIAN_FRONTEND=noninteractive PG_VERSION=${PG_VERSION} BASE_VERSION=${BASE_VERSION} - -ARG ONLYOFFICE_VALUE=onlyoffice - -RUN echo "#!/bin/sh\nexit 0" > /usr/sbin/policy-rc.d && \ - apt-get -y update && \ - apt-get -yq install wget apt-transport-https gnupg locales lsb-release && \ - wget -q -O /etc/apt/sources.list.d/mssql-release.list "https://packages.microsoft.com/config/ubuntu/$BASE_VERSION/prod.list" && \ - wget -q -O /tmp/microsoft.asc https://packages.microsoft.com/keys/microsoft.asc && \ - apt-key add /tmp/microsoft.asc && \ - gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg < /tmp/microsoft.asc && \ - apt-get -y update && \ - locale-gen en_US.UTF-8 && \ - echo ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true | debconf-set-selections && \ - ACCEPT_EULA=Y apt-get -yq install \ - adduser \ - apt-utils \ - bomstrip \ - certbot \ - cron \ - curl \ - htop \ - libaio1${PACKAGE_SUFFIX} \ - libasound2${PACKAGE_SUFFIX} \ - libboost-regex-dev \ - libcairo2 \ - libcurl3-gnutls \ - libcurl4 \ - libgtk-3-0 \ - libnspr4 \ - libnss3 \ - libstdc++6 \ - libxml2 \ - libxss1 \ - libxtst6 \ - mssql-tools18 \ - mysql-client \ - nano \ - net-tools \ - netcat-openbsd \ - nginx-extras \ - postgresql \ - postgresql-client \ - pwgen \ - rabbitmq-server \ - redis-server \ - sudo \ - supervisor \ - ttf-mscorefonts-installer \ - unixodbc-dev \ - unzip \ - xvfb \ - xxd \ - zlib1g || dpkg --configure -a && \ - # Added dpkg --configure -a to handle installation issues with rabbitmq-server on arm64 architecture - if [ $(ls -l /usr/share/fonts/truetype/msttcorefonts | wc -l) -ne 61 ]; \ - then echo 'msttcorefonts failed to download'; exit 1; fi && \ - echo "SERVER_ADDITIONAL_ERL_ARGS=\"+S 1:1\"" | tee -a /etc/rabbitmq/rabbitmq-env.conf && \ - sed -i "s/bind .*/bind 127.0.0.1/g" /etc/redis/redis.conf && \ - sed 's|\(application\/zip.*\)|\1\n application\/wasm wasm;|' -i /etc/nginx/mime.types && \ - pg_conftool $PG_VERSION main set listen_addresses 'localhost' && \ - service postgresql restart && \ - sudo -u postgres psql -c "CREATE USER $ONLYOFFICE_VALUE WITH password '$ONLYOFFICE_VALUE';" && \ - sudo -u postgres psql -c "CREATE DATABASE $ONLYOFFICE_VALUE OWNER $ONLYOFFICE_VALUE;" && \ - wget -O basic.zip ${OC_DOWNLOAD_URL}/instantclient-basic-linux.x64-${OC_FILE_SUFFIX}.zip && \ - wget -O sqlplus.zip ${OC_DOWNLOAD_URL}/instantclient-sqlplus-linux.x64-${OC_FILE_SUFFIX}.zip && \ - unzip -d /usr/share basic.zip && \ - unzip -d /usr/share sqlplus.zip && \ - mv /usr/share/instantclient_${OC_VER_DIR} /usr/share/instantclient && \ - service postgresql stop && \ - service redis-server stop && \ - service rabbitmq-server stop && \ - service supervisor stop && \ - service nginx stop && \ - rm -rf /var/lib/apt/lists/* - -COPY config/supervisor/supervisor /etc/init.d/ -COPY config/supervisor/ds/*.conf /etc/supervisor/conf.d/ -COPY run-document-server.sh /app/ds/run-document-server.sh -COPY oracle/sqlplus /usr/bin/sqlplus - -EXPOSE 80 443 - -ARG COMPANY_NAME=onlyoffice -ARG PRODUCT_NAME=documentserver -ARG PRODUCT_EDITION= -ARG PACKAGE_VERSION= -ARG TARGETARCH -ARG PACKAGE_BASEURL="http://download.onlyoffice.com/install/documentserver/linux" - -ENV COMPANY_NAME=$COMPANY_NAME \ - PRODUCT_NAME=$PRODUCT_NAME \ - PRODUCT_EDITION=$PRODUCT_EDITION \ - DS_PLUGIN_INSTALLATION=false \ - DS_DOCKER_INSTALLATION=true - -RUN PACKAGE_FILE="${COMPANY_NAME}-${PRODUCT_NAME}${PRODUCT_EDITION}${PACKAGE_VERSION:+_$PACKAGE_VERSION}_${TARGETARCH:-$(dpkg --print-architecture)}.deb" && \ - wget -q -P /tmp "$PACKAGE_BASEURL/$PACKAGE_FILE" && \ - apt-get -y update && \ - service postgresql start && \ - apt-get -yq install /tmp/$PACKAGE_FILE && \ - service postgresql stop && \ - chmod 755 /etc/init.d/supervisor && \ - sed "s/COMPANY_NAME/${COMPANY_NAME}/g" -i /etc/supervisor/conf.d/*.conf && \ - service supervisor stop && \ - chmod 755 /app/ds/*.sh && \ - printf "\nGO" >> "/var/www/$COMPANY_NAME/documentserver/server/schema/mssql/createdb.sql" && \ - printf "\nGO" >> "/var/www/$COMPANY_NAME/documentserver/server/schema/mssql/removetbl.sql" && \ - printf "\nexit" >> "/var/www/$COMPANY_NAME/documentserver/server/schema/oracle/createdb.sql" && \ - printf "\nexit" >> "/var/www/$COMPANY_NAME/documentserver/server/schema/oracle/removetbl.sql" && \ - rm -f /tmp/$PACKAGE_FILE && \ - rm -rf /var/log/$COMPANY_NAME && \ - rm -rf /var/lib/apt/lists/* - -VOLUME /var/log/$COMPANY_NAME /var/lib/$COMPANY_NAME /var/www/$COMPANY_NAME/Data /var/lib/postgresql /var/lib/rabbitmq /var/lib/redis /usr/share/fonts/truetype/custom - -ENTRYPOINT ["/app/ds/run-document-server.sh"] diff --git a/onlyoffice/LICENSE.txt b/onlyoffice/LICENSE.txt deleted file mode 100644 index dba13ed..0000000 --- a/onlyoffice/LICENSE.txt +++ /dev/null @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/onlyoffice/Makefile b/onlyoffice/Makefile deleted file mode 100644 index bb80cde..0000000 --- a/onlyoffice/Makefile +++ /dev/null @@ -1,66 +0,0 @@ -COMPANY_NAME ?= ONLYOFFICE -GIT_BRANCH ?= develop -PRODUCT_NAME ?= documentserver -PRODUCT_EDITION ?= -PRODUCT_VERSION ?= 0.0.0 -BUILD_NUMBER ?= 0 -BUILD_CHANNEL ?= nightly -ONLYOFFICE_VALUE ?= onlyoffice - -COMPANY_NAME_LOW = $(shell echo $(COMPANY_NAME) | tr A-Z a-z) - -PACKAGE_NAME := $(COMPANY_NAME_LOW)-$(PRODUCT_NAME)$(PRODUCT_EDITION) -PACKAGE_VERSION ?= $(PRODUCT_VERSION)-$(BUILD_NUMBER)~stretch -PACKAGE_BASEURL ?= https://s3.eu-west-1.amazonaws.com/repo-doc-onlyoffice-com/server/linux/debian - -ifeq ($(BUILD_CHANNEL),$(filter $(BUILD_CHANNEL),nightly test)) - DOCKER_TAG := $(PRODUCT_VERSION).$(BUILD_NUMBER) -else - DOCKER_TAG := $(PRODUCT_VERSION).$(BUILD_NUMBER)-$(subst /,-,$(GIT_BRANCH)) -endif - -DOCKER_ORG ?= $(COMPANY_NAME_LOW) -DOCKER_IMAGE := $(DOCKER_ORG)/4testing-$(PRODUCT_NAME)$(PRODUCT_EDITION) -DOCKER_DUMMY := $(COMPANY_NAME_LOW)-$(PRODUCT_NAME)$(PRODUCT_EDITION)__$(DOCKER_TAG).dummy -DOCKER_ARCH := $(COMPANY_NAME_LOW)-$(PRODUCT_NAME)_$(DOCKER_TAG).tar.gz - -.PHONY: all clean clean-docker image deploy docker - -$(DOCKER_DUMMY): - docker pull ubuntu:22.04 - docker build \ - --build-arg COMPANY_NAME=$(COMPANY_NAME_LOW) \ - --build-arg PRODUCT_NAME=$(PRODUCT_NAME) \ - --build-arg PRODUCT_EDITION=$(PRODUCT_EDITION) \ - --build-arg PACKAGE_VERSION=$(PACKAGE_VERSION) \ - --build-arg PACKAGE_BASEURL=$(PACKAGE_BASEURL) \ - --build-arg TARGETARCH=amd64 \ - --build-arg ONLYOFFICE_VALUE=$(ONLYOFFICE_VALUE) \ - -t $(DOCKER_IMAGE):$(DOCKER_TAG) . && \ - mkdir -p $$(dirname $@) && \ - echo "Done" > $@ - -$(DOCKER_ARCH): $(DOCKER_DUMMY) - docker save $(DOCKER_IMAGE):$(DOCKER_TAG) | \ - gzip > $@ - -all: image - -clean: - rm -rfv *.dummy *.tar.gz - -clean-docker: - docker rmi -f $$(docker images -q $(COMPANY_NAME_LOW)/*) || exit 0 - -image: $(DOCKER_DUMMY) - -deploy: $(DOCKER_DUMMY) - for i in {1..3}; do \ - docker push $(DOCKER_IMAGE):$(DOCKER_TAG) && break || sleep 1m; \ - done -ifeq ($(BUILD_CHANNEL),nightly) - docker tag $(DOCKER_IMAGE):$(DOCKER_TAG) $(DOCKER_IMAGE):latest - for i in {1..3}; do \ - docker push $(DOCKER_IMAGE):latest && break || sleep 1m; \ - done -endif diff --git a/onlyoffice/README.md b/onlyoffice/README.md deleted file mode 100644 index 298fe09..0000000 --- a/onlyoffice/README.md +++ /dev/null @@ -1,387 +0,0 @@ -* [Overview](#overview) -* [Functionality](#functionality) -* [Recommended System Requirements](#recommended-system-requirements) -* [Running Docker Image](#running-docker-image) -* [Configuring Docker Image](#configuring-docker-image) - - [Storing Data](#storing-data) - - [Running ONLYOFFICE Document Server on Different Port](#running-onlyoffice-document-server-on-different-port) - - [Running ONLYOFFICE Document Server using HTTPS](#running-onlyoffice-document-server-using-https) - + [Generation of Self Signed Certificates](#generation-of-self-signed-certificates) - + [Strengthening the Server Security](#strengthening-the-server-security) - + [Installation of the SSL Certificates](#installation-of-the-ssl-certificates) - + [Available Configuration Parameters](#available-configuration-parameters) -* [Installing ONLYOFFICE Document Server integrated with Community and Mail Servers](#installing-onlyoffice-document-server-integrated-with-community-and-mail-servers) -* [ONLYOFFICE Document Server ipv6 setup](#onlyoffice-document-server-ipv6-setup) -* [Issues](#issues) - - [Docker Issues](#docker-issues) - - [Document Server usage Issues](#document-server-usage-issues) -* [Project Information](#project-information) -* [User Feedback and Support](#user-feedback-and-support) - -## Overview - -ONLYOFFICE Document Server is an online office suite comprising viewers and editors for texts, spreadsheets and presentations, fully compatible with Office Open XML formats: .docx, .xlsx, .pptx and enabling collaborative editing in real time. - -Starting from version 6.0, Document Server is distributed as ONLYOFFICE Docs. It has [three editions](https://github.com/ONLYOFFICE/DocumentServer#onlyoffice-document-server-editions). With this image, you will install the free Community version. - -ONLYOFFICE Docs can be used as a part of ONLYOFFICE Workspace or with third-party sync&share solutions (e.g. Nextcloud, ownCloud, Seafile) to enable collaborative editing within their interface. - -***Important*** Please update `docker-engine` to latest version (`20.10.21` as of writing this doc) before using it. We use `ubuntu:22.04` as base image and it older versions of docker have compatibility problems with it - -## Functionality ## -* ONLYOFFICE Document Editor -* ONLYOFFICE Spreadsheet Editor -* ONLYOFFICE Presentation Editor -* ONLYOFFICE Documents application for iOS -* Collaborative editing -* Hieroglyph support -* Support for all the popular formats: DOC, DOCX, TXT, ODT, RTF, ODP, EPUB, ODS, XLS, XLSX, CSV, PPTX, HTML - -Integrating it with ONLYOFFICE Community Server you will be able to: -* view and edit files stored on Drive, Box, Dropbox, OneDrive, OwnCloud connected to ONLYOFFICE; -* share files; -* embed documents on a website; -* manage access rights to documents. - -## Recommended System Requirements - -* **RAM**: 4 GB or more -* **CPU**: dual-core 2 GHz or higher -* **Swap**: at least 2 GB -* **HDD**: at least 2 GB of free space -* **Distribution**: 64-bit Red Hat, CentOS or other compatible distributive with kernel version 3.8 or later, 64-bit Debian, Ubuntu or other compatible distributive with kernel version 3.8 or later -* **Docker**: version 1.9.0 or later - -## Running Docker Image - - sudo docker run -i -t -d -p 80:80 onlyoffice/documentserver - -Use this command if you wish to install ONLYOFFICE Document Server separately. To install ONLYOFFICE Document Server integrated with Community and Mail Servers, refer to the corresponding instructions below. - -## Configuring Docker Image - -### Storing Data - -All the data are stored in the specially-designated directories, **data volumes**, at the following location: -* **/var/log/onlyoffice** for ONLYOFFICE Document Server logs -* **/var/www/onlyoffice/Data** for certificates -* **/var/lib/onlyoffice** for file cache -* **/var/lib/postgresql** for database - -To get access to your data from outside the container, you need to mount the volumes. It can be done by specifying the '-v' option in the docker run command. - - sudo docker run -i -t -d -p 80:80 \ - -v /app/onlyoffice/DocumentServer/logs:/var/log/onlyoffice \ - -v /app/onlyoffice/DocumentServer/data:/var/www/onlyoffice/Data \ - -v /app/onlyoffice/DocumentServer/lib:/var/lib/onlyoffice \ - -v /app/onlyoffice/DocumentServer/rabbitmq:/var/lib/rabbitmq \ - -v /app/onlyoffice/DocumentServer/redis:/var/lib/redis \ - -v /app/onlyoffice/DocumentServer/db:/var/lib/postgresql onlyoffice/documentserver - -Normally, you do not need to store container data because the container's operation does not depend on its state. Saving data will be useful: -* For easy access to container data, such as logs -* To remove the limit on the size of the data inside the container -* When using services launched outside the container such as PostgreSQL, Redis, RabbitMQ - -### Running ONLYOFFICE Document Server on Different Port - -To change the port, use the -p command. E.g.: to make your portal accessible via port 8080 execute the following command: - - sudo docker run -i -t -d -p 8080:80 onlyoffice/documentserver - -### Running ONLYOFFICE Document Server using HTTPS - - sudo docker run -i -t -d -p 443:443 \ - -v /app/onlyoffice/DocumentServer/data:/var/www/onlyoffice/Data onlyoffice/documentserver - -Access to the onlyoffice application can be secured using SSL so as to prevent unauthorized access. While a CA certified SSL certificate allows for verification of trust via the CA, a self signed certificates can also provide an equal level of trust verification as long as each client takes some additional steps to verify the identity of your website. Below the instructions on achieving this are provided. - -To secure the application via SSL basically two things are needed: - -- **Private key (.key)** -- **SSL certificate (.crt)** - -So you need to create and install the following files: - - /app/onlyoffice/DocumentServer/data/certs/tls.key - /app/onlyoffice/DocumentServer/data/certs/tls.crt - -When using CA certified certificates (e.g [Let's encrypt](https://letsencrypt.org)), these files are provided to you by the CA. If you are using self-signed certificates you need to generate these files [yourself](#generation-of-self-signed-certificates). - -#### Using the automatically generated Let's Encrypt SSL Certificates - - sudo docker run -i -t -d -p 80:80 -p 443:443 \ - -e LETS_ENCRYPT_DOMAIN=your_domain -e LETS_ENCRYPT_MAIL=your_mail onlyoffice/documentserver - -If you want to get and extend Let's Encrypt SSL Certificates automatically just set LETS_ENCRYPT_DOMAIN and LETS_ENCRYPT_MAIL variables. - -#### Generation of Self Signed Certificates - -Generation of self-signed SSL certificates involves a simple 3 step procedure. - -**STEP 1**: Create the server private key - -```bash -openssl genrsa -out tls.key 2048 -``` - -**STEP 2**: Create the certificate signing request (CSR) - -```bash -openssl req -new -key tls.key -out tls.csr -``` - -**STEP 3**: Sign the certificate using the private key and CSR - -```bash -openssl x509 -req -days 365 -in tls.csr -signkey tls.key -out tls.crt -``` - -You have now generated an SSL certificate that's valid for 365 days. - -#### Strengthening the server security - -This section provides you with instructions to [strengthen your server security](https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html). -To achieve this you need to generate stronger DHE parameters. - -```bash -openssl dhparam -out dhparam.pem 2048 -``` - -#### Installation of the SSL Certificates - -Out of the four files generated above, you need to install the `tls.key`, `tls.crt` and `dhparam.pem` files at the onlyoffice server. The CSR file is not needed, but do make sure you safely backup the file (in case you ever need it again). - -The default path that the onlyoffice application is configured to look for the SSL certificates is at `/var/www/onlyoffice/Data/certs`, this can however be changed using the `SSL_KEY_PATH`, `SSL_CERTIFICATE_PATH` and `SSL_DHPARAM_PATH` configuration options. - -The `/var/www/onlyoffice/Data/` path is the path of the data store, which means that you have to create a folder named certs inside `/app/onlyoffice/DocumentServer/data/` and copy the files into it and as a measure of security you will update the permission on the `tls.key` file to only be readable by the owner. - -```bash -mkdir -p /app/onlyoffice/DocumentServer/data/certs -cp tls.key /app/onlyoffice/DocumentServer/data/certs/ -cp tls.crt /app/onlyoffice/DocumentServer/data/certs/ -cp dhparam.pem /app/onlyoffice/DocumentServer/data/certs/ -chmod 400 /app/onlyoffice/DocumentServer/data/certs/tls.key -``` - -You are now just one step away from having our application secured. - -#### Available Configuration Parameters - -*Please refer the docker run command options for the `--env-file` flag where you can specify all required environment variables in a single file. This will save you from writing a potentially long docker run command.* - -Below is the complete list of parameters that can be set using environment variables. - -- **ONLYOFFICE_HTTPS_HSTS_ENABLED**: Advanced configuration option for turning off the HSTS configuration. Applicable only when SSL is in use. Defaults to `true`. -- **ONLYOFFICE_HTTPS_HSTS_MAXAGE**: Advanced configuration option for setting the HSTS max-age in the onlyoffice nginx vHost configuration. Applicable only when SSL is in use. Defaults to `31536000`. -- **SSL_CERTIFICATE_PATH**: The path to the SSL certificate to use. Defaults to `/var/www/onlyoffice/Data/certs/tls.crt`. -- **SSL_KEY_PATH**: The path to the SSL certificate's private key. Defaults to `/var/www/onlyoffice/Data/certs/tls.key`. -- **SSL_DHPARAM_PATH**: The path to the Diffie-Hellman parameter. Defaults to `/var/www/onlyoffice/Data/certs/dhparam.pem`. -- **SSL_VERIFY_CLIENT**: Enable verification of client certificates using the `CA_CERTIFICATES_PATH` file. Defaults to `false` -- **NODE_EXTRA_CA_CERTS**: The [NODE_EXTRA_CA_CERTS](https://nodejs.org/api/cli.html#node_extra_ca_certsfile "Node.js documentation") to extend CAs with the extra certificates for Node.js. Defaults to `/var/www/onlyoffice/Data/certs/extra-ca-certs.pem`. -- **DB_TYPE**: The database type. Supported values are `postgres`, `mariadb`, `mysql`, `mssql` or `oracle`. Defaults to `postgres`. -- **DB_HOST**: The IP address or the name of the host where the database server is running. -- **DB_PORT**: The database server port number. -- **DB_NAME**: The name of a database to use. Should be existing on container startup. -- **DB_USER**: The new user name with superuser permissions for the database account. -- **DB_PWD**: The password set for the database account. -- **AMQP_URI**: The [AMQP URI](https://www.rabbitmq.com/uri-spec.html "RabbitMQ URI Specification") to connect to message broker server. -- **AMQP_TYPE**: The message broker type. Supported values are `rabbitmq` or `activemq`. Defaults to `rabbitmq`. -- **REDIS_SERVER_HOST**: The IP address or the name of the host where the Redis server is running. -- **REDIS_SERVER_PORT**: The Redis server port number. -- **REDIS_SERVER_PASS**: The Redis server password. The password is not set by default. -- **NGINX_WORKER_PROCESSES**: Defines the number of nginx worker processes. -- **NGINX_WORKER_CONNECTIONS**: Sets the maximum number of simultaneous connections that can be opened by a nginx worker process. -- **SECURE_LINK_SECRET**: Defines secret for the nginx config directive [secure_link_md5](https://nginx.org/en/docs/http/ngx_http_secure_link_module.html#secure_link_md5). Defaults to `random string`. -- **JWT_ENABLED**: Specifies the enabling the JSON Web Token validation by the ONLYOFFICE Document Server. Defaults to `true`. -- **JWT_SECRET**: Defines the secret key to validate the JSON Web Token in the request to the ONLYOFFICE Document Server. Defaults to random value. -- **JWT_HEADER**: Defines the http header that will be used to send the JSON Web Token. Defaults to `Authorization`. -- **JWT_IN_BODY**: Specifies the enabling the token validation in the request body to the ONLYOFFICE Document Server. Defaults to `false`. -- **WOPI_ENABLED**: Specifies the enabling the wopi handlers. Defaults to `false`. -- **ALLOW_META_IP_ADDRESS**: Defines if it is allowed to connect meta IP address or not. Defaults to `false`. -- **ALLOW_PRIVATE_IP_ADDRESS**: Defines if it is allowed to connect private IP address or not. Defaults to `false`. -- **USE_UNAUTHORIZED_STORAGE**: Set to `true`if using selfsigned certificates for your storage server e.g. Nextcloud. Defaults to `false` -- **GENERATE_FONTS**: When 'true' regenerates fonts list and the fonts thumbnails etc. at each start. Defaults to `true` -- **METRICS_ENABLED**: Specifies the enabling StatsD for ONLYOFFICE Document Server. Defaults to `false`. -- **METRICS_HOST**: Defines StatsD listening host. Defaults to `localhost`. -- **METRICS_PORT**: Defines StatsD listening port. Defaults to `8125`. -- **METRICS_PREFIX**: Defines StatsD metrics prefix for backend services. Defaults to `ds.`. -- **LETS_ENCRYPT_DOMAIN**: Defines the domain for Let's Encrypt certificate. -- **LETS_ENCRYPT_MAIL**: Defines the domain administator mail address for Let's Encrypt certificate. -- **PLUGINS_ENABLED**: Defines whether to enable default plugins. Defaults to `true`. - -## Installing ONLYOFFICE Document Server integrated with Community and Mail Servers - -ONLYOFFICE Document Server is a part of ONLYOFFICE Community Edition that comprises also Community Server and Mail Server. To install them, follow these easy steps: - -**STEP 1**: Create the `onlyoffice` network. - -```bash -docker network create --driver bridge onlyoffice -``` -Then launch containers on it using the 'docker run --net onlyoffice' option: - -**STEP 2**: Install MySQL. - -Follow [these steps](#installing-mysql) to install MySQL server. - -**STEP 3**: Generate JWT Secret - -JWT secret defines the secret key to validate the JSON Web Token in the request to the **ONLYOFFICE Document Server**. You can specify it yourself or easily get it using the command: -``` -JWT_SECRET=$(cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 12); -``` - -**STEP 4**: Install ONLYOFFICE Document Server. - -```bash -sudo docker run --net onlyoffice -i -t -d --restart=always --name onlyoffice-document-server \ - -e JWT_ENABLED=true \ - -e JWT_SECRET=${JWT_SECRET} \ - -e JWT_HEADER=AuthorizationJwt \ - -v /app/onlyoffice/DocumentServer/logs:/var/log/onlyoffice \ - -v /app/onlyoffice/DocumentServer/data:/var/www/onlyoffice/Data \ - -v /app/onlyoffice/DocumentServer/lib:/var/lib/onlyoffice \ - -v /app/onlyoffice/DocumentServer/db:/var/lib/postgresql \ - onlyoffice/documentserver -``` - -**STEP 5**: Install ONLYOFFICE Mail Server. - -For the mail server correct work you need to specify its hostname 'yourdomain.com'. - -```bash -sudo docker run --init --net onlyoffice --privileged -i -t -d --restart=always --name onlyoffice-mail-server -p 25:25 -p 143:143 -p 587:587 \ - -e MYSQL_SERVER=onlyoffice-mysql-server \ - -e MYSQL_SERVER_PORT=3306 \ - -e MYSQL_ROOT_USER=root \ - -e MYSQL_ROOT_PASSWD=my-secret-pw \ - -e MYSQL_SERVER_DB_NAME=onlyoffice_mailserver \ - -v /app/onlyoffice/MailServer/data:/var/vmail \ - -v /app/onlyoffice/MailServer/data/certs:/etc/pki/tls/mailserver \ - -v /app/onlyoffice/MailServer/logs:/var/log \ - -h yourdomain.com \ - onlyoffice/mailserver -``` - -The additional parameters for mail server are available [here](https://github.com/ONLYOFFICE/Docker-CommunityServer/blob/master/docker-compose.workspace_enterprise.yml#L87). - -To learn more, refer to the [ONLYOFFICE Mail Server documentation](https://github.com/ONLYOFFICE/Docker-MailServer "ONLYOFFICE Mail Server documentation"). - -**STEP 6**: Install ONLYOFFICE Community Server - -```bash -sudo docker run --net onlyoffice -i -t -d --privileged --restart=always --name onlyoffice-community-server -p 80:80 -p 443:443 -p 5222:5222 --cgroupns=host \ - -e MYSQL_SERVER_ROOT_PASSWORD=my-secret-pw \ - -e MYSQL_SERVER_DB_NAME=onlyoffice \ - -e MYSQL_SERVER_HOST=onlyoffice-mysql-server \ - -e MYSQL_SERVER_USER=onlyoffice_user \ - -e MYSQL_SERVER_PASS=onlyoffice_pass \ - - -e DOCUMENT_SERVER_PORT_80_TCP_ADDR=onlyoffice-document-server \ - -e DOCUMENT_SERVER_JWT_ENABLED=true \ - -e DOCUMENT_SERVER_JWT_SECRET=${JWT_SECRET} \ - -e DOCUMENT_SERVER_JWT_HEADER=AuthorizationJwt \ - - -e MAIL_SERVER_API_HOST=${MAIL_SERVER_IP} \ - -e MAIL_SERVER_DB_HOST=onlyoffice-mysql-server \ - -e MAIL_SERVER_DB_NAME=onlyoffice_mailserver \ - -e MAIL_SERVER_DB_PORT=3306 \ - -e MAIL_SERVER_DB_USER=root \ - -e MAIL_SERVER_DB_PASS=my-secret-pw \ - - -v /app/onlyoffice/CommunityServer/data:/var/www/onlyoffice/Data \ - -v /app/onlyoffice/CommunityServer/logs:/var/log/onlyoffice \ - -v /app/onlyoffice/CommunityServer/letsencrypt:/etc/letsencrypt \ - -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ - onlyoffice/communityserver -``` - -Where `${MAIL_SERVER_IP}` is the IP address for **ONLYOFFICE Mail Server**. You can easily get it using the command: -``` -MAIL_SERVER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' onlyoffice-mail-server) -``` - -Alternatively, you can use an automatic installation script to install the whole ONLYOFFICE Community Edition at once. For the mail server correct work you need to specify its hostname 'yourdomain.com'. - -**STEP 1**: Download the Community Edition Docker script file - -```bash -wget https://download.onlyoffice.com/install/opensource-install.sh -``` - -**STEP 2**: Install ONLYOFFICE Community Edition executing the following command: - -```bash -bash opensource-install.sh -md yourdomain.com -``` - -Or, use [docker-compose](https://docs.docker.com/compose/install "docker-compose"). For the mail server correct work you need to specify its hostname 'yourdomain.com'. Assuming you have docker-compose installed, execute the following command: - -```bash -wget https://raw.githubusercontent.com/ONLYOFFICE/Docker-CommunityServer/master/docker-compose.groups.yml -docker-compose up -d -``` - -## ONLYOFFICE Document Server ipv6 setup - -(Works and is supported only for Linux hosts) - -Docker does not currently provide ipv6 addresses to containers by default. This function is experimental now. - -To set up interaction via ipv6, you need to enable support for this feature in your Docker. For this you need: -- create the `/etc/docker/daemon.json` file with the following content: - -``` -{ -"ipv6": true, -"fixed-cidr-v6": "2001:db8:abc1::/64" -} -``` -- restart docker with the following command: `systemctl restart docker` - -After that, all running containers receive an ipv6 address and have an inet6 interface. - -You can check your default bridge network and see the field there -`EnableIPv6=true`. A new ipv6 subnet will also be added. - -For more information, visit the official [Docker manual site](https://docs.docker.com/config/daemon/ipv6/) - -## Issues - -### Docker Issues - -As a relatively new project Docker is being worked on and actively developed by its community. So it's recommended to use the latest version of Docker, because the issues that you encounter might have already been fixed with a newer Docker release. - -The known Docker issue with ONLYOFFICE Document Server with rpm-based distributives is that sometimes the processes fail to start inside Docker container. Fedora and RHEL/CentOS users should try disabling selinux with setenforce 0. If it fixes the issue then you can either stick with SELinux disabled which is not recommended by RedHat, or switch to using Ubuntu. - -### Document Server usage issues - -Due to the operational characteristic, **Document Server** saves a document only after the document has been closed by all the users who edited it. To avoid data loss, you must forcefully disconnect the **Document Server** users when you need to stop **Document Server** in cases of the application update, server reboot etc. To do that, execute the following script on the server where **Document Server** is installed: - -``` -sudo docker exec documentserver-prepare4shutdown.sh -``` - -Please note, that both executing the script and disconnecting users may take a long time (up to 5 minutes). - -## Project Information - -Official website: [https://www.onlyoffice.com](https://www.onlyoffice.com/?utm_source=github&utm_medium=cpc&utm_campaign=GitHubDockerDS) - -Code repository: [https://github.com/ONLYOFFICE/DocumentServer](https://github.com/ONLYOFFICE/DocumentServer "https://github.com/ONLYOFFICE/DocumentServer") - -Docker Image: [https://github.com/ONLYOFFICE/Docker-DocumentServer](https://github.com/ONLYOFFICE/Docker-DocumentServer "https://github.com/ONLYOFFICE/Docker-DocumentServer") - -License: [GNU AGPL v3.0](https://help.onlyoffice.com/products/files/doceditor.aspx?fileid=4358397&doc=K0ZUdlVuQzQ0RFhhMzhZRVN4ZFIvaHlhUjN2eS9XMXpKR1M5WEppUk1Gcz0_IjQzNTgzOTci0 "GNU AGPL v3.0") - -Free version vs commercial builds comparison: https://github.com/ONLYOFFICE/DocumentServer#onlyoffice-document-server-editions - -SaaS version: [https://www.onlyoffice.com/cloud-office.aspx](https://www.onlyoffice.com/cloud-office.aspx?utm_source=github&utm_medium=cpc&utm_campaign=GitHubDockerDS) - -## User Feedback and Support - -If you have any problems with or questions about this image, please visit our official forum to find answers to your questions: [forum.onlyoffice.com][1] or you can ask and answer ONLYOFFICE development questions on [Stack Overflow][2]. - - [1]: https://forum.onlyoffice.com - [2]: https://stackoverflow.com/questions/tagged/onlyoffice diff --git a/onlyoffice/config/supervisor/ds/ds-converter.conf b/onlyoffice/config/supervisor/ds/ds-converter.conf deleted file mode 100644 index 21daf98..0000000 --- a/onlyoffice/config/supervisor/ds/ds-converter.conf +++ /dev/null @@ -1,13 +0,0 @@ -[program:converter] -command=/var/www/COMPANY_NAME/documentserver/server/FileConverter/converter -directory=/var/www/COMPANY_NAME/documentserver/server/FileConverter -user=ds -environment=NODE_ENV=production-linux,NODE_CONFIG_DIR=/etc/COMPANY_NAME/documentserver,NODE_DISABLE_COLORS=1,APPLICATION_NAME=COMPANY_NAME,LD_LIBRARY_PATH=/var/www/COMPANY_NAME/documentserver/server/FileConverter/bin -stdout_logfile=/var/log/COMPANY_NAME/documentserver/converter/out.log -stdout_logfile_backups=0 -stdout_logfile_maxbytes=0 -stderr_logfile=/var/log/COMPANY_NAME/documentserver/converter/err.log -stderr_logfile_backups=0 -stderr_logfile_maxbytes=0 -autostart=true -autorestart=true diff --git a/onlyoffice/config/supervisor/ds/ds-docservice.conf b/onlyoffice/config/supervisor/ds/ds-docservice.conf deleted file mode 100644 index 7cb28f7..0000000 --- a/onlyoffice/config/supervisor/ds/ds-docservice.conf +++ /dev/null @@ -1,13 +0,0 @@ -[program:docservice] -command=/var/www/COMPANY_NAME/documentserver/server/DocService/docservice -directory=/var/www/COMPANY_NAME/documentserver/server/DocService -user=ds -environment=NODE_ENV=production-linux,NODE_CONFIG_DIR=/etc/COMPANY_NAME/documentserver,NODE_DISABLE_COLORS=1,APPLICATION_NAME=COMPANY_NAME -stdout_logfile=/var/log/COMPANY_NAME/documentserver/docservice/out.log -stdout_logfile_backups=0 -stdout_logfile_maxbytes=0 -stderr_logfile=/var/log/COMPANY_NAME/documentserver/docservice/err.log -stderr_logfile_backups=0 -stderr_logfile_maxbytes=0 -autostart=true -autorestart=true diff --git a/onlyoffice/config/supervisor/ds/ds-example.conf b/onlyoffice/config/supervisor/ds/ds-example.conf deleted file mode 100644 index 44fa688..0000000 --- a/onlyoffice/config/supervisor/ds/ds-example.conf +++ /dev/null @@ -1,14 +0,0 @@ -[program:example] -command=/var/www/COMPANY_NAME/documentserver-example/example -directory=/var/www/COMPANY_NAME/documentserver-example/ -user=ds -environment=NODE_ENV=production-linux,NODE_CONFIG_DIR=/etc/COMPANY_NAME/documentserver-example,NODE_DISABLE_COLORS=1 -stdout_logfile=/var/log/COMPANY_NAME/documentserver-example/out.log -stdout_logfile_backups=0 -stdout_logfile_maxbytes=0 -stderr_logfile=/var/log/COMPANY_NAME/documentserver-example/err.log -stderr_logfile_backups=0 -stderr_logfile_maxbytes=0 -autostart=false -autorestart=true -redirect_stderr=true diff --git a/onlyoffice/config/supervisor/ds/ds-metrics.conf b/onlyoffice/config/supervisor/ds/ds-metrics.conf deleted file mode 100644 index d9c7576..0000000 --- a/onlyoffice/config/supervisor/ds/ds-metrics.conf +++ /dev/null @@ -1,13 +0,0 @@ -[program:metrics] -command=/var/www/COMPANY_NAME/documentserver/server/Metrics/metrics ./config/config.js -directory=/var/www/COMPANY_NAME/documentserver/server/Metrics -user=ds -environment=NODE_DISABLE_COLORS=1 -stdout_logfile=/var/log/COMPANY_NAME/documentserver/metrics/out.log -stdout_logfile_backups=0 -stdout_logfile_maxbytes=0 -stderr_logfile=/var/log/COMPANY_NAME/documentserver/metrics/err.log -stderr_logfile_backups=0 -stderr_logfile_maxbytes=0 -autostart=false -autorestart=false diff --git a/onlyoffice/config/supervisor/ds/ds.conf b/onlyoffice/config/supervisor/ds/ds.conf deleted file mode 100644 index c9179df..0000000 --- a/onlyoffice/config/supervisor/ds/ds.conf +++ /dev/null @@ -1,2 +0,0 @@ -[group:ds] -programs=docservice,converter,metrics,example diff --git a/onlyoffice/config/supervisor/supervisor b/onlyoffice/config/supervisor/supervisor deleted file mode 100644 index 1e612e0..0000000 --- a/onlyoffice/config/supervisor/supervisor +++ /dev/null @@ -1,176 +0,0 @@ -#! /bin/sh -# -# skeleton example file to build /etc/init.d/ scripts. -# This file should be used to construct scripts for /etc/init.d. -# -# Written by Miquel van Smoorenburg . -# Modified for Debian -# by Ian Murdock . -# Further changes by Javier Fernandez-Sanguino -# -# Version: @(#)skeleton 1.9 26-Feb-2001 miquels@cistron.nl -# -### BEGIN INIT INFO -# Provides: supervisor -# Required-Start: $remote_fs $network $named -# Required-Stop: $remote_fs $network $named -# Default-Start: 2 3 4 5 -# Default-Stop: 0 1 6 -# Short-Description: Start/stop supervisor -# Description: Start/stop supervisor daemon and its configured -# subprocesses. -### END INIT INFO - -. /lib/lsb/init-functions - -PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin -DAEMON=/usr/bin/supervisord -NAME=supervisord -DESC=supervisor - -test -x $DAEMON || exit 0 - -LOGDIR=/var/log/supervisor -PIDFILE=/var/run/$NAME.pid -PS_COUNT=0 -DODTIME=5 # Time to wait for the server to die, in seconds - # If this value is set too low you might not - # let some servers to die gracefully and - # 'restart' will not work - -# Include supervisor defaults if available -if [ -f /etc/default/supervisor ] ; then - . /etc/default/supervisor -fi -DAEMON_OPTS="-c /etc/supervisor/supervisord.conf $DAEMON_OPTS" - -set -e - -running_pid() -{ - # Check if a given process pid's cmdline matches a given name - pid=$1 - name=$2 - [ -z "$pid" ] && return 1 - [ ! -d /proc/$pid ] && return 1 - (cat /proc/$pid/cmdline | tr "\000" "\n"|grep -q $name) || return 1 - return 0 -} - -running() -{ -# Check if the process is running looking at /proc -# (works for all users) - - # No pidfile, probably no daemon present - [ ! -f "$PIDFILE" ] && return 1 - # Obtain the pid and check it against the binary name - pid=`cat $PIDFILE` - running_pid $pid $DAEMON || return 1 - return 0 -} - -force_stop() { -# Forcefully kill the process - [ ! -f "$PIDFILE" ] && return - if running ; then - kill -15 $pid - # Is it really dead? - [ -n "$DODTIME" ] && sleep "$DODTIME"s - if running ; then - kill -9 $pid - [ -n "$DODTIME" ] && sleep "$DODTIME"s - if running ; then - echo "Cannot kill $LABEL (pid=$pid)!" - exit 1 - fi - fi - fi - rm -f $PIDFILE - return 0 -} - -get_pid() { - PS_COUNT=$(pgrep -fc $DAEMON || true) -} - -case "$1" in - start) - get_pid - if [ $PS_COUNT -eq 0 ]; then - rm -f "$PIDFILE" - fi - echo -n "Starting $DESC: " - start-stop-daemon --start --quiet --pidfile $PIDFILE \ - --startas $DAEMON -- $DAEMON_OPTS - test -f $PIDFILE || sleep 1 - if running ; then - echo "$NAME." - else - echo " ERROR." - fi - ;; - stop) - echo -n "Stopping $DESC: " - start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE - echo "$NAME." - ;; - force-stop) - echo -n "Forcefully stopping $DESC: " - force_stop - if ! running ; then - echo "$NAME." - else - echo " ERROR." - fi - ;; - #reload) - # - # If the daemon can reload its config files on the fly - # for example by sending it SIGHUP, do it here. - # - # If the daemon responds to changes in its config file - # directly anyway, make this a do-nothing entry. - # - # echo "Reloading $DESC configuration files." - # start-stop-daemon --stop --signal 1 --quiet --pidfile \ - # /var/run/$NAME.pid --exec $DAEMON - #;; - force-reload) - # - # If the "reload" option is implemented, move the "force-reload" - # option to the "reload" entry above. If not, "force-reload" is - # just the same as "restart" except that it does nothing if the - # daemon isn't already running. - # check wether $DAEMON is running. If so, restart - start-stop-daemon --stop --test --quiet --pidfile $PIDFILE \ - --startas $DAEMON \ - && $0 restart \ - || exit 0 - ;; - restart) - echo -n "Restarting $DESC: " - start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE - [ -n "$DODTIME" ] && sleep $DODTIME - start-stop-daemon --start --quiet --pidfile $PIDFILE \ - --startas $DAEMON -- $DAEMON_OPTS - echo "$NAME." - ;; - status) - echo -n "$LABEL is " - if running ; then - echo "running" - else - echo " not running." - exit 1 - fi - ;; - *) - N=/etc/init.d/$NAME - # echo "Usage: $N {start|stop|restart|reload|force-reload}" >&2 - echo "Usage: $N {start|stop|restart|force-reload|status|force-stop}" >&2 - exit 1 - ;; -esac - -exit 0 diff --git a/onlyoffice/docker-bake.hcl b/onlyoffice/docker-bake.hcl deleted file mode 100644 index 7db339c..0000000 --- a/onlyoffice/docker-bake.hcl +++ /dev/null @@ -1,174 +0,0 @@ -variable "TAG" { - default = "" -} - -variable "SHORTER_TAG" { - default = "" -} - -variable "SHORTEST_TAG" { - default = "" -} - -variable "PULL_TAG" { - default = "" -} - -variable "COMPANY_NAME" { - default = "" -} - -variable "PREFIX_NAME" { - default = "" -} - -variable "PRODUCT_EDITION" { - default = "" -} - -variable "PRODUCT_NAME" { - default = "" -} - -variable "PACKAGE_VERSION" { - default = "" -} - -variable "DOCKERFILE" { - default = "" -} - -variable "PLATFORM" { - default = "" -} - -variable "PACKAGE_BASEURL" { - default = "" -} - -variable "PACKAGE_FILE" { - default = "" -} - -variable "BUILD_CHANNEL" { - default = "" -} - -variable "PUSH_MAJOR" { - default = "false" -} - -variable "LATEST" { - default = "false" -} - -### ↓ Variables for UCS build ↓ - -variable "BASE_VERSION" { - default = "" -} - -variable "PACKAGE_SUFFIX" { - default = "" -} - -variable "PG_VERSION" { - default = "" -} - -variable "UCS_REBUILD" { - default = "" -} - -variable "UCS_PREFIX" { - default = "" -} - -### ↑ Variables for UCS build ↑ - -target "documentserver" { - target = "documentserver" - dockerfile = "${DOCKERFILE}" - tags = [ - "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}", - equal("nightly",BUILD_CHANNEL) ? "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:latest": "", - ] - platforms = ["${PLATFORM}"] - args = { - "COMPANY_NAME": "${COMPANY_NAME}" - "PRODUCT_NAME": "${PRODUCT_NAME}" - "PRODUCT_EDITION": "${PRODUCT_EDITION}" - "PACKAGE_VERSION": "${PACKAGE_VERSION}" - "PACKAGE_BASEURL": "${PACKAGE_BASEURL}" - "PLATFORM": "${PLATFORM}" - } -} - -target "documentserver-stable" { - target = "documentserver-stable" - dockerfile = "production.dockerfile" - tags = ["docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}", - "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${SHORTER_TAG}", - "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${SHORTEST_TAG}", - "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:latest", - equal("-ee",PRODUCT_EDITION) ? "docker.io/${COMPANY_NAME}4enterprise/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}": "",] - platforms = ["linux/amd64", "linux/arm64"] - args = { - "PULL_TAG": "${PULL_TAG}" - "COMPANY_NAME": "${COMPANY_NAME}" - "PRODUCT_NAME": "${PRODUCT_NAME}" - "PRODUCT_EDITION": "${PRODUCT_EDITION}" - } -} - -target "documentserver-ucs" { - target = "documentserver" - dockerfile = "${DOCKERFILE}" - tags = [ - "docker.io/${COMPANY_NAME}/${PRODUCT_NAME}${PRODUCT_EDITION}-ucs:${TAG}" - ] - platforms = ["linux/amd64", "linux/arm64"] - args = { - "PRODUCT_EDITION": "${PRODUCT_EDITION}" - "PRODUCT_NAME": "${PRODUCT_NAME}" - "COMPANY_NAME": "${COMPANY_NAME}" - "PACKAGE_VERSION": "${PACKAGE_VERSION}" - "PACKAGE_BASEURL": "${PACKAGE_BASEURL}" - "PACKAGE_SUFFIX": "${PACKAGE_SUFFIX}" - "BASE_VERSION": "${BASE_VERSION}" - "PG_VERSION": "${PG_VERSION}" - } -} - -target "documentserver-nonexample" { - target = "documentserver-nonexample" - dockerfile = "production.dockerfile" - tags = [ "docker.io/${COMPANY_NAME}/${PRODUCT_NAME}${PREFIX_NAME}${PRODUCT_EDITION}:${TAG}-nonexample" ] - platforms = ["linux/amd64", "linux/arm64"] - args = { - "PULL_TAG": "${PULL_TAG}" - "COMPANY_NAME": "${COMPANY_NAME}" - "PRODUCT_NAME": "${PRODUCT_NAME}" - "PRODUCT_EDITION": "${PRODUCT_EDITION}" - } -} - -target "documentserver-stable-rebuild" { - target = "documentserver-stable-rebuild" - dockerfile = "production.dockerfile" - tags = equal("true",UCS_REBUILD) ? ["docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}-ucs:${TAG}",] : [ - "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}", - equal("",PREFIX_NAME) ? "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${SHORTER_TAG}": "", - equal("true",PUSH_MAJOR) ? "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${SHORTEST_TAG}": "", - equal("",PREFIX_NAME) && equal("true",LATEST) ? "docker.io/${COMPANY_NAME}/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:latest": "", - equal("-ee",PRODUCT_EDITION) && equal("",PREFIX_NAME) ? "docker.io/${COMPANY_NAME}4enterprise/${PREFIX_NAME}${PRODUCT_NAME}${PRODUCT_EDITION}:${TAG}": "", - ] - platforms = ["linux/amd64", "linux/arm64"] - args = { - "UCS_PREFIX": "${UCS_PREFIX}" - "PULL_TAG": "${PULL_TAG}" - "COMPANY_NAME": "${COMPANY_NAME}" - "PRODUCT_NAME": "${PRODUCT_NAME}" - "PRODUCT_EDITION": "${PRODUCT_EDITION}" - } -} diff --git a/onlyoffice/docker-compose.yml b/onlyoffice/docker-compose.yml deleted file mode 100644 index 27670cb..0000000 --- a/onlyoffice/docker-compose.yml +++ /dev/null @@ -1,55 +0,0 @@ -services: - onlyoffice-documentserver: - build: - context: . - container_name: onlyoffice-documentserver - depends_on: - - onlyoffice-postgresql - - onlyoffice-rabbitmq - environment: - - DB_TYPE=postgres - - DB_HOST=onlyoffice-postgresql - - DB_PORT=5432 - - DB_NAME=onlyoffice - - DB_USER=onlyoffice - - AMQP_URI=amqp://guest:guest@onlyoffice-rabbitmq - # Uncomment strings below to enable the JSON Web Token validation. - #- JWT_ENABLED=true - #- JWT_SECRET=secret - #- JWT_HEADER=Authorization - #- JWT_IN_BODY=true - ports: - - '80:80' - - '443:443' - stdin_open: true - restart: always - stop_grace_period: 60s - volumes: - - /var/www/onlyoffice/Data - - /var/log/onlyoffice - - /var/lib/onlyoffice/documentserver/App_Data/cache/files - - /var/www/onlyoffice/documentserver-example/public/files - - /usr/share/fonts - - onlyoffice-rabbitmq: - container_name: onlyoffice-rabbitmq - image: rabbitmq - restart: always - expose: - - '5672' - - onlyoffice-postgresql: - container_name: onlyoffice-postgresql - image: postgres:12 - environment: - - POSTGRES_DB=onlyoffice - - POSTGRES_USER=onlyoffice - - POSTGRES_HOST_AUTH_METHOD=trust - restart: always - expose: - - '5432' - volumes: - - postgresql_data:/var/lib/postgresql - -volumes: - postgresql_data: diff --git a/onlyoffice/oracle/sqlplus b/onlyoffice/oracle/sqlplus deleted file mode 100755 index 3bd1bd5..0000000 --- a/onlyoffice/oracle/sqlplus +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -CLIENTDIR=/usr/share/instantclient -export LD_LIBRARY_PATH=$CLIENTDIR -$CLIENTDIR/sqlplus $@ - diff --git a/onlyoffice/production.dockerfile b/onlyoffice/production.dockerfile deleted file mode 100644 index 24c6569..0000000 --- a/onlyoffice/production.dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -### Arguments avavlivable only for FROM instruction ### -ARG PULL_TAG=latest -ARG COMPANY_NAME=onlyoffice -ARG PRODUCT_EDITION= -### Rebuild arguments -ARG UCS_PREFIX= -ARG IMAGE=${COMPANY_NAME}/documentserver${PRODUCT_EDITION}${UCS_PREFIX}:${PULL_TAG} - -### Build main-release ### - -FROM ${COMPANY_NAME}/4testing-documentserver${PRODUCT_EDITION}:${PULL_TAG} as documentserver-stable - -### Rebuild stable images with secure updates -FROM ${IMAGE} as documentserver-stable-rebuild -RUN echo "This is rebuild" \ - && apt-get update -y \ - && apt-get upgrade -y - -### Build nonexample ### - -FROM ${COMPANY_NAME}/documentserver${PRODUCT_EDITION}:${PULL_TAG} as documentserver-nonexample - -ARG COMPANY_NAME=onlyoffice -ARG PRODUCT_NAME=documentserver -ARG DS_SUPERVISOR_CONF=/etc/supervisor/conf.d/ds.conf - -### Remove all documentserver-example data ### - -RUN rm -rf /var/www/$COMPANY_NAME/$PRODUCT_NAME-example \ - && rm -rf /etc/$COMPANY_NAME/$PRODUCT_NAME-example \ - && rm -f $DS_SUPERVISOR_CONF \ - && rm -f /etc/nginx/includes/ds-example.conf \ - && ln -s /etc/$COMPANY_NAME/$PRODUCT_NAME/supervisor/ds.conf $DS_SUPERVISOR_CONF diff --git a/onlyoffice/run-document-server.sh b/onlyoffice/run-document-server.sh deleted file mode 100644 index f751b3c..0000000 --- a/onlyoffice/run-document-server.sh +++ /dev/null @@ -1,795 +0,0 @@ -#!/bin/bash - -umask 0022 - -start_process() { - "$@" & - CHILD=$!; wait "$CHILD"; CHILD=""; -} - -function clean_exit { - [[ -z "$CHILD" ]] || kill -s SIGTERM "$CHILD" 2>/dev/null - if [ ${ONLYOFFICE_DATA_CONTAINER} == "false" ] && \ - [ ${ONLYOFFICE_DATA_CONTAINER_HOST} == "localhost" ]; then - /usr/bin/documentserver-prepare4shutdown.sh - fi - exit -} - -trap clean_exit SIGTERM SIGQUIT SIGABRT SIGINT - -# Define '**' behavior explicitly -shopt -s globstar - -APP_DIR="/var/www/${COMPANY_NAME}/documentserver" -DATA_DIR="/var/www/${COMPANY_NAME}/Data" -PRIVATE_DATA_DIR="${DATA_DIR}/.private" -DS_RELEASE_DATE="${PRIVATE_DATA_DIR}/ds_release_date" -LOG_DIR="/var/log/${COMPANY_NAME}" -DS_LOG_DIR="${LOG_DIR}/documentserver" -LIB_DIR="/var/lib/${COMPANY_NAME}" -DS_LIB_DIR="${LIB_DIR}/documentserver" -CONF_DIR="/etc/${COMPANY_NAME}/documentserver" -SUPERVISOR_CONF_DIR="/etc/supervisor/conf.d" -IS_UPGRADE="false" -PLUGINS_ENABLED=${PLUGINS_ENABLED:-true} - -ONLYOFFICE_DATA_CONTAINER=${ONLYOFFICE_DATA_CONTAINER:-false} -ONLYOFFICE_DATA_CONTAINER_HOST=${ONLYOFFICE_DATA_CONTAINER_HOST:-localhost} -ONLYOFFICE_DATA_CONTAINER_PORT=80 - -RELEASE_DATE="$(stat -c="%y" ${APP_DIR}/server/DocService/docservice | sed -r 's/=([0-9]+)-([0-9]+)-([0-9]+) ([0-9:.+ ]+)/\1-\2-\3/')"; -if [ -f ${DS_RELEASE_DATE} ]; then - PREV_RELEASE_DATE=$(head -n 1 ${DS_RELEASE_DATE}) -else - PREV_RELEASE_DATE="0" -fi - -if [ "${RELEASE_DATE}" != "${PREV_RELEASE_DATE}" ]; then - if [ ${ONLYOFFICE_DATA_CONTAINER} != "true" ]; then - IS_UPGRADE="true"; - fi -fi - -SSL_CERTIFICATES_DIR="/usr/share/ca-certificates/ds" -mkdir -p ${SSL_CERTIFICATES_DIR} -if find "${DATA_DIR}/certs" -type f \( -name "*.crt" -o -name "*.pem" \) -print -quit >/dev/null 2>&1; then - cp -f ${DATA_DIR}/certs/* ${SSL_CERTIFICATES_DIR} - chmod 644 ${SSL_CERTIFICATES_DIR}/*.{crt,pem} 2>/dev/null - chmod 400 ${SSL_CERTIFICATES_DIR}/*.key 2>/dev/null -fi - -if [[ -z $SSL_CERTIFICATE_PATH ]] && [[ -f ${SSL_CERTIFICATES_DIR}/${COMPANY_NAME}.crt ]]; then - SSL_CERTIFICATE_PATH=${SSL_CERTIFICATES_DIR}/${COMPANY_NAME}.crt -else - SSL_CERTIFICATE_PATH=${SSL_CERTIFICATE_PATH:-${SSL_CERTIFICATES_DIR}/tls.crt} -fi -if [[ -z $SSL_KEY_PATH ]] && [[ -f ${SSL_CERTIFICATES_DIR}/${COMPANY_NAME}.key ]]; then - SSL_KEY_PATH=${SSL_CERTIFICATES_DIR}/${COMPANY_NAME}.key -else - SSL_KEY_PATH=${SSL_KEY_PATH:-${SSL_CERTIFICATES_DIR}/tls.key} -fi - -#When set, the well known "root" CAs will be extended with the extra certificates in file -NODE_EXTRA_CA_CERTS=${NODE_EXTRA_CA_CERTS:-${SSL_CERTIFICATES_DIR}/extra-ca-certs.pem} -if [[ -f ${NODE_EXTRA_CA_CERTS} ]]; then - NODE_EXTRA_ENVIRONMENT="${NODE_EXTRA_CA_CERTS}" -elif [[ -f ${SSL_CERTIFICATE_PATH} ]]; then - SSL_CERTIFICATE_SUBJECT=$(openssl x509 -subject -noout -in "${SSL_CERTIFICATE_PATH}" | sed 's/subject=//') - SSL_CERTIFICATE_ISSUER=$(openssl x509 -issuer -noout -in "${SSL_CERTIFICATE_PATH}" | sed 's/issuer=//') - - #Add self-signed certificate to trusted list for validating Docs requests to the test example - if [[ -n $SSL_CERTIFICATE_SUBJECT && $SSL_CERTIFICATE_SUBJECT == $SSL_CERTIFICATE_ISSUER ]]; then - NODE_EXTRA_ENVIRONMENT="${SSL_CERTIFICATE_PATH}" - fi -fi - -if [[ -n $NODE_EXTRA_ENVIRONMENT ]]; then - sed -i "s|^environment=.*$|&,NODE_EXTRA_CA_CERTS=${NODE_EXTRA_ENVIRONMENT}|" /etc/supervisor/conf.d/*.conf -fi - -CA_CERTIFICATES_PATH=${CA_CERTIFICATES_PATH:-${SSL_CERTIFICATES_DIR}/ca-certificates.pem} -SSL_DHPARAM_PATH=${SSL_DHPARAM_PATH:-${SSL_CERTIFICATES_DIR}/dhparam.pem} -SSL_VERIFY_CLIENT=${SSL_VERIFY_CLIENT:-off} -USE_UNAUTHORIZED_STORAGE=${USE_UNAUTHORIZED_STORAGE:-false} -ONLYOFFICE_HTTPS_HSTS_ENABLED=${ONLYOFFICE_HTTPS_HSTS_ENABLED:-true} -ONLYOFFICE_HTTPS_HSTS_MAXAGE=${ONLYOFFICE_HTTPS_HSTS_MAXAGE:-31536000} -SYSCONF_TEMPLATES_DIR="/app/ds/setup/config" - -NGINX_CONFD_PATH="/etc/nginx/conf.d"; -NGINX_ONLYOFFICE_PATH="${CONF_DIR}/nginx" -NGINX_ONLYOFFICE_CONF="${NGINX_ONLYOFFICE_PATH}/ds.conf" -NGINX_ONLYOFFICE_EXAMPLE_PATH="${CONF_DIR}-example/nginx" -NGINX_ONLYOFFICE_EXAMPLE_CONF="${NGINX_ONLYOFFICE_EXAMPLE_PATH}/includes/ds-example.conf" - -NGINX_CONFIG_PATH="/etc/nginx/nginx.conf" -NGINX_WORKER_PROCESSES=${NGINX_WORKER_PROCESSES:-1} -# Limiting the maximum number of simultaneous connections due to possible memory shortage -LIMIT=$(ulimit -n); [ $LIMIT -gt 1048576 ] && LIMIT=1048576 -NGINX_WORKER_CONNECTIONS=${NGINX_WORKER_CONNECTIONS:-$LIMIT} -RABBIT_CONNECTIONS=${RABBIT_CONNECTIONS:-$LIMIT} - -JWT_ENABLED=${JWT_ENABLED:-true} - -# validate user's vars before usinig in json -if [ "${JWT_ENABLED}" == "true" ]; then - JWT_ENABLED="true" -else - JWT_ENABLED="false" -fi - -[ -z $JWT_SECRET ] && JWT_MESSAGE='JWT is enabled by default. A random secret is generated automatically. Run the command "docker exec $(sudo docker ps -q) sudo documentserver-jwt-status.sh" to get information about JWT.' - -JWT_SECRET=${JWT_SECRET:-$(pwgen -s 32)} -JWT_HEADER=${JWT_HEADER:-Authorization} -JWT_IN_BODY=${JWT_IN_BODY:-false} - -WOPI_ENABLED=${WOPI_ENABLED:-false} -ALLOW_META_IP_ADDRESS=${ALLOW_META_IP_ADDRESS:-false} -ALLOW_PRIVATE_IP_ADDRESS=${ALLOW_PRIVATE_IP_ADDRESS:-false} - -GENERATE_FONTS=${GENERATE_FONTS:-true} - -if [[ ${PRODUCT_NAME}${PRODUCT_EDITION} == "documentserver" ]]; then - REDIS_ENABLED=false -else - REDIS_ENABLED=true -fi - -ONLYOFFICE_DEFAULT_CONFIG=${CONF_DIR}/local.json -ONLYOFFICE_LOG4JS_CONFIG=${CONF_DIR}/log4js/production.json -ONLYOFFICE_EXAMPLE_CONFIG=${CONF_DIR}-example/local.json - -JSON_BIN=${APP_DIR}/npm/json -JSON="${JSON_BIN} -q -f ${ONLYOFFICE_DEFAULT_CONFIG}" -JSON_LOG="${JSON_BIN} -q -f ${ONLYOFFICE_LOG4JS_CONFIG}" -JSON_EXAMPLE="${JSON_BIN} -q -f ${ONLYOFFICE_EXAMPLE_CONFIG}" - -LOCAL_SERVICES=() - -PG_ROOT=/var/lib/postgresql -PG_NAME=main -PGDATA=${PG_ROOT}/${PG_VERSION}/${PG_NAME} -PG_NEW_CLUSTER=false -RABBITMQ_DATA=/var/lib/rabbitmq -REDIS_DATA=/var/lib/redis - -if [ "${LETS_ENCRYPT_DOMAIN}" != "" -a "${LETS_ENCRYPT_MAIL}" != "" ]; then - LETSENCRYPT_ROOT_DIR="/etc/letsencrypt/live" - SSL_CERTIFICATE_PATH=${LETSENCRYPT_ROOT_DIR}/${LETS_ENCRYPT_DOMAIN}/fullchain.pem - SSL_KEY_PATH=${LETSENCRYPT_ROOT_DIR}/${LETS_ENCRYPT_DOMAIN}/privkey.pem -fi - -read_setting(){ - deprecated_var POSTGRESQL_SERVER_HOST DB_HOST - deprecated_var POSTGRESQL_SERVER_PORT DB_PORT - deprecated_var POSTGRESQL_SERVER_DB_NAME DB_NAME - deprecated_var POSTGRESQL_SERVER_USER DB_USER - deprecated_var POSTGRESQL_SERVER_PASS DB_PWD - deprecated_var RABBITMQ_SERVER_URL AMQP_URI - deprecated_var AMQP_SERVER_URL AMQP_URI - deprecated_var AMQP_SERVER_TYPE AMQP_TYPE - - METRICS_ENABLED="${METRICS_ENABLED:-false}" - METRICS_HOST="${METRICS_HOST:-localhost}" - METRICS_PORT="${METRICS_PORT:-8125}" - METRICS_PREFIX="${METRICS_PREFIX:-.ds}" - - DB_HOST=${DB_HOST:-${POSTGRESQL_SERVER_HOST:-$(${JSON} services.CoAuthoring.sql.dbHost)}} - DB_TYPE=${DB_TYPE:-$(${JSON} services.CoAuthoring.sql.type)} - case $DB_TYPE in - "postgres") - DB_PORT=${DB_PORT:-"5432"} - ;; - "mariadb"|"mysql") - DB_PORT=${DB_PORT:-"3306"} - ;; - "dameng") - DB_PORT=${DB_PORT:-"5236"} - ;; - "mssql") - DB_PORT=${DB_PORT:-"1433"} - ;; - "oracle") - DB_PORT=${DB_PORT:-"1521"} - ;; - "") - DB_PORT=${DB_PORT:-${POSTGRESQL_SERVER_PORT:-$(${JSON} services.CoAuthoring.sql.dbPort)}} - ;; - *) - echo "ERROR: unknown database type" - exit 1 - ;; - esac - DB_NAME=${DB_NAME:-${POSTGRESQL_SERVER_DB_NAME:-$(${JSON} services.CoAuthoring.sql.dbName)}} - DB_USER=${DB_USER:-${POSTGRESQL_SERVER_USER:-$(${JSON} services.CoAuthoring.sql.dbUser)}} - DB_PWD=${DB_PWD:-${POSTGRESQL_SERVER_PASS:-$(${JSON} services.CoAuthoring.sql.dbPass)}} - - RABBITMQ_SERVER_URL=${RABBITMQ_SERVER_URL:-$(${JSON} rabbitmq.url)} - AMQP_URI=${AMQP_URI:-${AMQP_SERVER_URL:-${RABBITMQ_SERVER_URL}}} - AMQP_TYPE=${AMQP_TYPE:-${AMQP_SERVER_TYPE:-rabbitmq}} - parse_rabbitmq_url ${AMQP_URI} - - REDIS_SERVER_HOST=${REDIS_SERVER_HOST:-$(${JSON} services.CoAuthoring.redis.host)} - REDIS_SERVER_PORT=${REDIS_SERVER_PORT:-6379} - - DS_LOG_LEVEL=${DS_LOG_LEVEL:-$(${JSON_LOG} categories.default.level)} -} - -deprecated_var() { - if [[ -n ${!1} ]]; then - echo "Variable $1 is deprecated. Use $2 instead." - fi -} - -parse_rabbitmq_url(){ - local amqp=$1 - - # extract the protocol - local proto="$(echo $amqp | grep :// | sed -e's,^\(.*://\).*,\1,g')" - # remove the protocol - local url="$(echo ${amqp/$proto/})" - - # extract the user and password (if any) - local userpass="`echo $url | grep @ | cut -d@ -f1`" - local pass=`echo $userpass | grep : | cut -d: -f2` - - local user - if [ -n "$pass" ]; then - user=`echo $userpass | grep : | cut -d: -f1` - else - user=$userpass - fi - - # extract the host - local hostport="$(echo ${url/$userpass@/} | cut -d/ -f1)" - # by request - try to extract the port - local port="$(echo $hostport | grep : | sed -r 's_^.*:+|/.*$__g')" - - local host - if [ -n "$port" ]; then - host=`echo $hostport | grep : | cut -d: -f1` - else - host=$hostport - port="5672" - fi - - # extract the path (if any) - local path="$(echo $url | grep / | cut -d/ -f2-)" - - AMQP_SERVER_PROTO=${proto:0:-3} - AMQP_SERVER_HOST=$host - AMQP_SERVER_USER=$user - AMQP_SERVER_PASS=$pass - AMQP_SERVER_PORT=$port -} - -waiting_for_connection(){ - until nc -z -w 3 "$1" "$2"; do - >&2 echo "Waiting for connection to the $1 host on port $2" - sleep 1 - done -} - -waiting_for_db_ready(){ - case $DB_TYPE in - "oracle") - PDB="XEPDB1" - ORACLE_SQL="sqlplus $DB_USER/$DB_PWD@//$DB_HOST:$DB_PORT/$PDB" - DB_TEST="echo \"SELECT version FROM V\$INSTANCE;\" | $ORACLE_SQL 2>/dev/null | grep \"Connected\" | wc -l" - ;; - *) - return - ;; - esac - - for (( i=1; i <= 10; i++ )); do - RES=$(eval $DB_TEST) - if [ "$RES" -ne "0" ]; then - echo "Database is ready" - break - fi - sleep 5 - done -} - -waiting_for_db(){ - waiting_for_connection $DB_HOST $DB_PORT - waiting_for_db_ready -} - -waiting_for_amqp(){ - waiting_for_connection ${AMQP_SERVER_HOST} ${AMQP_SERVER_PORT} -} - -waiting_for_redis(){ - waiting_for_connection ${REDIS_SERVER_HOST} ${REDIS_SERVER_PORT} -} -waiting_for_datacontainer(){ - waiting_for_connection ${ONLYOFFICE_DATA_CONTAINER_HOST} ${ONLYOFFICE_DATA_CONTAINER_PORT} -} - -update_statsd_settings(){ - ${JSON} -I -e "if(this.statsd===undefined)this.statsd={};" - ${JSON} -I -e "this.statsd.useMetrics = '${METRICS_ENABLED}'" - ${JSON} -I -e "this.statsd.host = '${METRICS_HOST}'" - ${JSON} -I -e "this.statsd.port = '${METRICS_PORT}'" - ${JSON} -I -e "this.statsd.prefix = '${METRICS_PREFIX}'" - sed -i -E "s/(autostart|autorestart)=.*$/\1=${METRICS_ENABLED}/g" ${SUPERVISOR_CONF_DIR}/ds-metrics.conf -} - -update_db_settings(){ - ${JSON} -I -e "this.services.CoAuthoring.sql.type = '${DB_TYPE}'" - ${JSON} -I -e "this.services.CoAuthoring.sql.dbHost = '${DB_HOST}'" - ${JSON} -I -e "this.services.CoAuthoring.sql.dbPort = '${DB_PORT}'" - ${JSON} -I -e "this.services.CoAuthoring.sql.dbName = '${DB_NAME}'" - ${JSON} -I -e "this.services.CoAuthoring.sql.dbUser = '${DB_USER}'" - ${JSON} -I -e "this.services.CoAuthoring.sql.dbPass = '${DB_PWD}'" -} - -update_rabbitmq_setting(){ - if [ "${AMQP_TYPE}" == "rabbitmq" ]; then - ${JSON} -I -e "if(this.queue===undefined)this.queue={};" - ${JSON} -I -e "this.queue.type = 'rabbitmq'" - ${JSON} -I -e "this.rabbitmq.url = '${AMQP_URI}'" - fi - - if [ "${AMQP_TYPE}" == "activemq" ]; then - ${JSON} -I -e "if(this.queue===undefined)this.queue={};" - ${JSON} -I -e "this.queue.type = 'activemq'" - ${JSON} -I -e "if(this.activemq===undefined)this.activemq={};" - ${JSON} -I -e "if(this.activemq.connectOptions===undefined)this.activemq.connectOptions={};" - - ${JSON} -I -e "this.activemq.connectOptions.host = '${AMQP_SERVER_HOST}'" - - if [ ! "${AMQP_SERVER_PORT}" == "" ]; then - ${JSON} -I -e "this.activemq.connectOptions.port = '${AMQP_SERVER_PORT}'" - else - ${JSON} -I -e "delete this.activemq.connectOptions.port" - fi - - if [ ! "${AMQP_SERVER_USER}" == "" ]; then - ${JSON} -I -e "this.activemq.connectOptions.username = '${AMQP_SERVER_USER}'" - else - ${JSON} -I -e "delete this.activemq.connectOptions.username" - fi - - if [ ! "${AMQP_SERVER_PASS}" == "" ]; then - ${JSON} -I -e "this.activemq.connectOptions.password = '${AMQP_SERVER_PASS}'" - else - ${JSON} -I -e "delete this.activemq.connectOptions.password" - fi - - case "${AMQP_SERVER_PROTO}" in - amqp+ssl|amqps) - ${JSON} -I -e "this.activemq.connectOptions.transport = 'tls'" - ;; - *) - ${JSON} -I -e "delete this.activemq.connectOptions.transport" - ;; - esac - fi -} - -update_redis_settings(){ - ${JSON} -I -e "if(this.services.CoAuthoring.redis===undefined)this.services.CoAuthoring.redis={};" - ${JSON} -I -e "this.services.CoAuthoring.redis.host = '${REDIS_SERVER_HOST}'" - ${JSON} -I -e "this.services.CoAuthoring.redis.port = '${REDIS_SERVER_PORT}'" - - if [ -n "${REDIS_SERVER_PASS}" ]; then - ${JSON} -I -e "this.services.CoAuthoring.redis.options = {'password':'${REDIS_SERVER_PASS}'}" - fi - -} - -update_ds_settings(){ - ${JSON} -I -e "this.services.CoAuthoring.token.enable.browser = ${JWT_ENABLED}" - ${JSON} -I -e "this.services.CoAuthoring.token.enable.request.inbox = ${JWT_ENABLED}" - ${JSON} -I -e "this.services.CoAuthoring.token.enable.request.outbox = ${JWT_ENABLED}" - - ${JSON} -I -e "this.services.CoAuthoring.secret.inbox.string = '${JWT_SECRET}'" - ${JSON} -I -e "this.services.CoAuthoring.secret.outbox.string = '${JWT_SECRET}'" - ${JSON} -I -e "this.services.CoAuthoring.secret.session.string = '${JWT_SECRET}'" - - ${JSON} -I -e "this.services.CoAuthoring.token.inbox.header = '${JWT_HEADER}'" - ${JSON} -I -e "this.services.CoAuthoring.token.outbox.header = '${JWT_HEADER}'" - - ${JSON} -I -e "this.services.CoAuthoring.token.inbox.inBody = ${JWT_IN_BODY}" - ${JSON} -I -e "this.services.CoAuthoring.token.outbox.inBody = ${JWT_IN_BODY}" - - if [ -f "${ONLYOFFICE_EXAMPLE_CONFIG}" ]; then - ${JSON_EXAMPLE} -I -e "this.server.token.enable = ${JWT_ENABLED}" - ${JSON_EXAMPLE} -I -e "this.server.token.secret = '${JWT_SECRET}'" - ${JSON_EXAMPLE} -I -e "this.server.token.authorizationHeader = '${JWT_HEADER}'" - fi - - if [ "${USE_UNAUTHORIZED_STORAGE}" == "true" ]; then - ${JSON} -I -e "if(this.services.CoAuthoring.requestDefaults===undefined)this.services.CoAuthoring.requestDefaults={}" - ${JSON} -I -e "if(this.services.CoAuthoring.requestDefaults.rejectUnauthorized===undefined)this.services.CoAuthoring.requestDefaults.rejectUnauthorized=false" - fi - - WOPI_PRIVATE_KEY="${DATA_DIR}/wopi_private.key" - WOPI_PUBLIC_KEY="${DATA_DIR}/wopi_public.key" - - [ ! -f "${WOPI_PRIVATE_KEY}" ] && echo -n "Generating WOPI private key..." && openssl genpkey -algorithm RSA -outform PEM -out "${WOPI_PRIVATE_KEY}" >/dev/null 2>&1 && echo "Done" - [ ! -f "${WOPI_PUBLIC_KEY}" ] && echo -n "Generating WOPI public key..." && openssl rsa -RSAPublicKey_out -in "${WOPI_PRIVATE_KEY}" -outform "MS PUBLICKEYBLOB" -out "${WOPI_PUBLIC_KEY}" >/dev/null 2>&1 && echo "Done" - WOPI_MODULUS=$(openssl rsa -pubin -inform "MS PUBLICKEYBLOB" -modulus -noout -in "${WOPI_PUBLIC_KEY}" | sed 's/Modulus=//' | xxd -r -p | openssl base64 -A) - WOPI_EXPONENT=$(openssl rsa -pubin -inform "MS PUBLICKEYBLOB" -text -noout -in "${WOPI_PUBLIC_KEY}" | grep -oP '(?<=Exponent: )\d+') - - ${JSON} -I -e "if(this.wopi===undefined)this.wopi={};" - ${JSON} -I -e "this.wopi.enable = ${WOPI_ENABLED}" - ${JSON} -I -e "this.wopi.privateKey = '$(awk '{printf "%s\\n", $0}' ${WOPI_PRIVATE_KEY})'" - ${JSON} -I -e "this.wopi.privateKeyOld = '$(awk '{printf "%s\\n", $0}' ${WOPI_PRIVATE_KEY})'" - ${JSON} -I -e "this.wopi.publicKey = '$(openssl base64 -in ${WOPI_PUBLIC_KEY} -A)'" - ${JSON} -I -e "this.wopi.publicKeyOld = '$(openssl base64 -in ${WOPI_PUBLIC_KEY} -A)'" - ${JSON} -I -e "this.wopi.modulus = '${WOPI_MODULUS}'" - ${JSON} -I -e "this.wopi.modulusOld = '${WOPI_MODULUS}'" - ${JSON} -I -e "this.wopi.exponent = ${WOPI_EXPONENT}" - ${JSON} -I -e "this.wopi.exponentOld = ${WOPI_EXPONENT}" - - if [ "${ALLOW_META_IP_ADDRESS}" = "true" ] || [ "${ALLOW_PRIVATE_IP_ADDRESS}" = "true" ]; then - ${JSON} -I -e "if(this.services.CoAuthoring['request-filtering-agent']===undefined)this.services.CoAuthoring['request-filtering-agent']={}" - [ "${ALLOW_META_IP_ADDRESS}" = "true" ] && ${JSON} -I -e "this.services.CoAuthoring['request-filtering-agent'].allowMetaIPAddress = true" - [ "${ALLOW_PRIVATE_IP_ADDRESS}" = "true" ] && ${JSON} -I -e "this.services.CoAuthoring['request-filtering-agent'].allowPrivateIPAddress = true" - fi -} - -create_postgresql_cluster(){ - local pg_conf_dir=/etc/postgresql/${PG_VERSION}/${PG_NAME} - local postgresql_conf=$pg_conf_dir/postgresql.conf - local hba_conf=$pg_conf_dir/pg_hba.conf - - mv $postgresql_conf $postgresql_conf.backup - mv $hba_conf $hba_conf.backup - - pg_createcluster ${PG_VERSION} ${PG_NAME} -} - -create_postgresql_db(){ - sudo -u postgres psql -c "CREATE USER $DB_USER WITH password '"$DB_PWD"';" - sudo -u postgres psql -c "CREATE DATABASE $DB_NAME OWNER $DB_USER;" -} - -create_mssql_db(){ - MSSQL="/opt/mssql-tools18/bin/sqlcmd -S $DB_HOST,$DB_PORT" - - $MSSQL -U $DB_USER -P "$DB_PWD" -C -Q "IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '$DB_NAME') BEGIN CREATE DATABASE $DB_NAME; END" -} - -create_db_tbl() { - case $DB_TYPE in - "postgres") - create_postgresql_tbl - ;; - "mariadb"|"mysql") - create_mysql_tbl - ;; - "mssql") - create_mssql_tbl - ;; - "oracle") - create_oracle_tbl - ;; - esac -} - -upgrade_db_tbl() { - case $DB_TYPE in - "postgres") - upgrade_postgresql_tbl - ;; - "mariadb"|"mysql") - upgrade_mysql_tbl - ;; - "mssql") - upgrade_mssql_tbl - ;; - "oracle") - upgrade_oracle_tbl - ;; - esac -} - -upgrade_postgresql_tbl() { - if [ -n "$DB_PWD" ]; then - export PGPASSWORD=$DB_PWD - fi - - PSQL="psql -q -h$DB_HOST -p$DB_PORT -d$DB_NAME -U$DB_USER -w" - - $PSQL -f "$APP_DIR/server/schema/postgresql/removetbl.sql" - $PSQL -f "$APP_DIR/server/schema/postgresql/createdb.sql" -} - -upgrade_mysql_tbl() { - CONNECTION_PARAMS="-h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PWD -w" - MYSQL="mysql -q $CONNECTION_PARAMS" - - $MYSQL $DB_NAME < "$APP_DIR/server/schema/mysql/removetbl.sql" >/dev/null 2>&1 - $MYSQL $DB_NAME < "$APP_DIR/server/schema/mysql/createdb.sql" >/dev/null 2>&1 -} - -upgrade_mssql_tbl() { - CONN_PARAMS="-U $DB_USER -P "$DB_PWD" -C" - MSSQL="/opt/mssql-tools18/bin/sqlcmd -S $DB_HOST,$DB_PORT $CONN_PARAMS" - - $MSSQL < "$APP_DIR/server/schema/mssql/removetbl.sql" >/dev/null 2>&1 - $MSSQL < "$APP_DIR/server/schema/mssql/createdb.sql" >/dev/null 2>&1 -} - -upgrade_oracle_tbl() { - PDB="XEPDB1" - ORACLE_SQL="sqlplus $DB_USER/$DB_PWD@//$DB_HOST:$DB_PORT/$PDB" - - $ORACLE_SQL @$APP_DIR/server/schema/oracle/removetbl.sql >/dev/null 2>&1 - $ORACLE_SQL @$APP_DIR/server/schema/oracle/createdb.sql >/dev/null 2>&1 -} - -create_postgresql_tbl() { - if [ -n "$DB_PWD" ]; then - export PGPASSWORD=$DB_PWD - fi - - PSQL="psql -q -h$DB_HOST -p$DB_PORT -d$DB_NAME -U$DB_USER -w" - $PSQL -f "$APP_DIR/server/schema/postgresql/createdb.sql" -} - -create_mysql_tbl() { - CONNECTION_PARAMS="-h$DB_HOST -P$DB_PORT -u$DB_USER -p$DB_PWD -w" - MYSQL="mysql -q $CONNECTION_PARAMS" - - # Create db on remote server - $MYSQL -e "CREATE DATABASE IF NOT EXISTS $DB_NAME DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;" >/dev/null 2>&1 - - $MYSQL $DB_NAME < "$APP_DIR/server/schema/mysql/createdb.sql" >/dev/null 2>&1 -} - -create_mssql_tbl() { - create_mssql_db - - CONN_PARAMS="-U $DB_USER -P "$DB_PWD" -C" - MSSQL="/opt/mssql-tools18/bin/sqlcmd -S $DB_HOST,$DB_PORT $CONN_PARAMS" - - $MSSQL < "$APP_DIR/server/schema/mssql/createdb.sql" >/dev/null 2>&1 -} - -create_oracle_tbl() { - PDB="XEPDB1" - ORACLE_SQL="sqlplus $DB_USER/$DB_PWD@//$DB_HOST:$DB_PORT/$PDB" - - $ORACLE_SQL @$APP_DIR/server/schema/oracle/createdb.sql >/dev/null 2>&1 -} - -update_welcome_page() { - WELCOME_PAGE="${APP_DIR}-example/welcome/docker.html" - if [[ -e $WELCOME_PAGE ]]; then - DOCKER_CONTAINER_ID=$(basename $(cat /proc/1/cpuset)) - (( ${#DOCKER_CONTAINER_ID} < 12 )) && DOCKER_CONTAINER_ID=$(hostname) - if (( ${#DOCKER_CONTAINER_ID} >= 12 )); then - if [[ -x $(command -v docker) ]]; then - DOCKER_CONTAINER_NAME=$(docker inspect --format="{{.Name}}" $DOCKER_CONTAINER_ID) - sed 's/$(sudo docker ps -q)/'"${DOCKER_CONTAINER_NAME#/}"'/' -i $WELCOME_PAGE - JWT_MESSAGE=$(echo $JWT_MESSAGE | sed 's/$(sudo docker ps -q)/'"${DOCKER_CONTAINER_NAME#/}"'/') - else - sed 's/$(sudo docker ps -q)/'"${DOCKER_CONTAINER_ID::12}"'/' -i $WELCOME_PAGE - JWT_MESSAGE=$(echo $JWT_MESSAGE | sed 's/$(sudo docker ps -q)/'"${DOCKER_CONTAINER_ID::12}"'/') - fi - fi - fi -} - -update_nginx_settings(){ - # Set up nginx - sed 's/^worker_processes.*/'"worker_processes ${NGINX_WORKER_PROCESSES};"'/' -i ${NGINX_CONFIG_PATH} - sed 's/worker_connections.*/'"worker_connections ${NGINX_WORKER_CONNECTIONS};"'/' -i ${NGINX_CONFIG_PATH} - sed 's/access_log.*/'"access_log off;"'/' -i ${NGINX_CONFIG_PATH} - - # setup HTTPS - if [ -f "${SSL_CERTIFICATE_PATH}" -a -f "${SSL_KEY_PATH}" ]; then - cp -f ${NGINX_ONLYOFFICE_PATH}/ds-ssl.conf.tmpl ${NGINX_ONLYOFFICE_CONF} - - # configure nginx - sed 's,{{SSL_CERTIFICATE_PATH}},'"${SSL_CERTIFICATE_PATH}"',' -i ${NGINX_ONLYOFFICE_CONF} - sed 's,{{SSL_KEY_PATH}},'"${SSL_KEY_PATH}"',' -i ${NGINX_ONLYOFFICE_CONF} - - # turn on http2 - sed 's,\(443 ssl\),\1 http2,' -i ${NGINX_ONLYOFFICE_CONF} - - # if dhparam path is valid, add to the config, otherwise remove the option - if [ -r "${SSL_DHPARAM_PATH}" ]; then - sed 's,\(\#* *\)\?\(ssl_dhparam \).*\(;\)$,'"\2${SSL_DHPARAM_PATH}\3"',' -i ${NGINX_ONLYOFFICE_CONF} - else - sed '/ssl_dhparam/d' -i ${NGINX_ONLYOFFICE_CONF} - fi - - sed 's,\(ssl_verify_client \).*\(;\)$,'"\1${SSL_VERIFY_CLIENT}\2"',' -i ${NGINX_ONLYOFFICE_CONF} - - if [ -f "${CA_CERTIFICATES_PATH}" ]; then - sed '/ssl_verify_client/a '"ssl_client_certificate ${CA_CERTIFICATES_PATH}"';' -i ${NGINX_ONLYOFFICE_CONF} - fi - - if [ "${ONLYOFFICE_HTTPS_HSTS_ENABLED}" == "true" ]; then - sed 's,\(max-age=\).*\(;\)$,'"\1${ONLYOFFICE_HTTPS_HSTS_MAXAGE}\2"',' -i ${NGINX_ONLYOFFICE_CONF} - else - sed '/max-age=/d' -i ${NGINX_ONLYOFFICE_CONF} - fi - else - ln -sf ${NGINX_ONLYOFFICE_PATH}/ds.conf.tmpl ${NGINX_ONLYOFFICE_CONF} - fi - - # check if ipv6 supported otherwise remove it from nginx config - if [ ! -f /proc/net/if_inet6 ]; then - sed '/listen\s\+\[::[0-9]*\].\+/d' -i $NGINX_ONLYOFFICE_CONF - fi - - if [ -f "${NGINX_ONLYOFFICE_EXAMPLE_CONF}" ]; then - sed 's/linux/docker/' -i ${NGINX_ONLYOFFICE_EXAMPLE_CONF} - fi - - start_process documentserver-update-securelink.sh -s ${SECURE_LINK_SECRET:-$(pwgen -s 20)} -r false -} - -update_log_settings(){ - ${JSON_LOG} -I -e "this.categories.default.level = '${DS_LOG_LEVEL}'" -} - -update_logrotate_settings(){ - sed 's|\(^su\b\).*|\1 root root|' -i /etc/logrotate.conf -} - -update_release_date(){ - mkdir -p ${PRIVATE_DATA_DIR} - echo ${RELEASE_DATE} > ${DS_RELEASE_DATE} -} - -# create base folders -for i in converter docservice metrics; do - mkdir -p "${DS_LOG_DIR}/$i" -done - -mkdir -p ${DS_LOG_DIR}-example - -# create app folders -for i in ${DS_LIB_DIR}/App_Data/cache/files ${DS_LIB_DIR}/App_Data/docbuilder ${DS_LIB_DIR}-example/files; do - mkdir -p "$i" -done - -# change folder rights -for i in ${DS_LOG_DIR} ${DS_LOG_DIR}-example ${LIB_DIR}; do - chown -R ds:ds "$i" - chmod -R 755 "$i" -done - -if [ ${ONLYOFFICE_DATA_CONTAINER_HOST} = "localhost" ]; then - - read_setting - - if [ $METRICS_ENABLED = "true" ]; then - update_statsd_settings - fi - - update_welcome_page - - update_log_settings - - update_ds_settings - - # update settings by env variables - if [ $DB_HOST != "localhost" ]; then - update_db_settings - waiting_for_db - create_db_tbl - else - # change rights for postgres directory - chown -R postgres:postgres ${PG_ROOT} - chmod -R 700 ${PG_ROOT} - - # create new db if it isn't exist - if [ ! -d ${PGDATA} ]; then - create_postgresql_cluster - PG_NEW_CLUSTER=true - fi - LOCAL_SERVICES+=("postgresql") - fi - - if [ ${AMQP_SERVER_HOST} != "localhost" ]; then - update_rabbitmq_setting - else - # change rights for rabbitmq directory - chown -R rabbitmq:rabbitmq ${RABBITMQ_DATA} - chmod -R go=rX,u=rwX ${RABBITMQ_DATA} - if [ -f ${RABBITMQ_DATA}/.erlang.cookie ]; then - chmod 400 ${RABBITMQ_DATA}/.erlang.cookie - fi - - echo "ulimit -n $RABBIT_CONNECTIONS" >> /etc/default/rabbitmq-server - - LOCAL_SERVICES+=("rabbitmq-server") - # allow Rabbitmq startup after container kill - rm -rf /var/run/rabbitmq - fi - - if [ ${REDIS_ENABLED} = "true" ]; then - if [ ${REDIS_SERVER_HOST} != "localhost" ]; then - update_redis_settings - else - # change rights for redis directory - chown -R redis:redis ${REDIS_DATA} - chmod -R 750 ${REDIS_DATA} - - LOCAL_SERVICES+=("redis-server") - fi - fi -else - # no need to update settings just wait for remote data - waiting_for_datacontainer - - # read settings after the data container in ready state - # to prevent get unconfigureted data - read_setting - - update_welcome_page -fi - -find /etc/${COMPANY_NAME} ! -path '*logrotate*' -exec chown ds:ds {} \; - -#start needed local services -for i in ${LOCAL_SERVICES[@]}; do - service $i start -done - -if [ ${PG_NEW_CLUSTER} = "true" ]; then - create_postgresql_db - create_postgresql_tbl -fi - -if [ ${ONLYOFFICE_DATA_CONTAINER} != "true" ]; then - waiting_for_db - waiting_for_amqp - if [ ${REDIS_ENABLED} = "true" ]; then - waiting_for_redis - fi - - if [ "${IS_UPGRADE}" = "true" ]; then - upgrade_db_tbl - update_release_date - fi - - update_nginx_settings - - service supervisor start - - # start cron to enable log rotating - update_logrotate_settings - service cron start -fi - -# Fix to resolve the `unknown "cache_tag" variable` error -start_process documentserver-flush-cache.sh -r false - -# nginx used as a proxy, and as data container status service. -# it run in all cases. -service nginx start - -if [ "${LETS_ENCRYPT_DOMAIN}" != "" -a "${LETS_ENCRYPT_MAIL}" != "" ]; then - if [ ! -f "${SSL_CERTIFICATE_PATH}" -a ! -f "${SSL_KEY_PATH}" ]; then - start_process documentserver-letsencrypt.sh ${LETS_ENCRYPT_MAIL} ${LETS_ENCRYPT_DOMAIN} - fi -fi - -# Regenerate the fonts list and the fonts thumbnails -if [ "${GENERATE_FONTS}" == "true" ]; then - start_process documentserver-generate-allfonts.sh ${ONLYOFFICE_DATA_CONTAINER} -fi - -if [ "${PLUGINS_ENABLED}" = "true" ]; then - echo -n Installing plugins, please wait... - start_process documentserver-pluginsmanager.sh -r false --update=\"${APP_DIR}/sdkjs-plugins/plugin-list-default.json\" >/dev/null - echo Done -fi - -start_process documentserver-static-gzip.sh ${ONLYOFFICE_DATA_CONTAINER} - -echo "${JWT_MESSAGE}" - -start_process tail -f /var/log/${COMPANY_NAME}/**/*.log diff --git a/onlyoffice/tests/README.md b/onlyoffice/tests/README.md deleted file mode 100644 index c17b546..0000000 --- a/onlyoffice/tests/README.md +++ /dev/null @@ -1,3 +0,0 @@ -The files in this folder are intended for use in integration auto-tests. - -All credentials are strictly for testing purposes only. \ No newline at end of file diff --git a/onlyoffice/tests/activemq.yml b/onlyoffice/tests/activemq.yml deleted file mode 100644 index 5781c64..0000000 --- a/onlyoffice/tests/activemq.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../. - environment: - - AMQP_TYPE=${AMQP_TYPE:-activemq} - - AMQP_URI=${AMQP_URI:-amqp://guest:guest@onlyoffice-activemq} - stdin_open: true - restart: always - ports: - - '80:80' - - '443:443' - networks: - - onlyoffice - - onlyoffice-activemq: - container_name: onlyoffice-activemq - image: webcenter/activemq:${ACTIVEMQ_VERSION:-5.14.3} - environment: - - ACTIVEMQ_USERS_guest=${ACTIVEMQ_USERS_guest:-guest} - - ACTIVEMQ_GROUPS_owners=${ACTIVEMQ_GROUPS_owners:-guest} - restart: always - networks: - - onlyoffice - expose: - - '5672' - -networks: - onlyoffice: - driver: 'bridge' diff --git a/onlyoffice/tests/certs-customized.yml b/onlyoffice/tests/certs-customized.yml deleted file mode 100644 index af73081..0000000 --- a/onlyoffice/tests/certs-customized.yml +++ /dev/null @@ -1,18 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../. - environment: - - SSL_CERTIFICATE_PATH=${SSL_CERTIFICATE_PATH:-/var/www/onlyoffice/Data/certs/tls.crt} - - SSL_KEY_PATH=${SSL_KEY_PATH:-/var/www/onlyoffice/Data/certs/tls.key} - - CA_CERTIFICATES_PATH=${CA_CERTIFICATES_PATH:-/var/www/onlyoffice/Data/certs/ca-certificates.pem} - - SSL_DHPARAM_PATH=${SSL_DHPARAM_PATH:-/var/www/onlyoffice/Data/certs/dhparam.pem} - stdin_open: true - restart: always - ports: - - '80:80' - - '443:443' - volumes: - - ./data:/var/www/onlyoffice/Data diff --git a/onlyoffice/tests/certs.yml b/onlyoffice/tests/certs.yml deleted file mode 100644 index 77d37ce..0000000 --- a/onlyoffice/tests/certs.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../. - stdin_open: true - restart: always - ports: - - '80:80' - - '443:443' - volumes: - - ./data:/var/www/onlyoffice/Data diff --git a/onlyoffice/tests/damengdb/README.md b/onlyoffice/tests/damengdb/README.md deleted file mode 100644 index c7f9c97..0000000 --- a/onlyoffice/tests/damengdb/README.md +++ /dev/null @@ -1,19 +0,0 @@ -## Stand Documentserver with damengdb - -### How it works - -For deploy stand, you need: - -**STEP 1**: Build you own images, do it with command: - -```bash -docker compose build -``` - -**STEP 2**: Wait build and when it finish deploy with command: - -```bash -docker compose up -d -``` - -Thats all. diff --git a/onlyoffice/tests/damengdb/damengdb.Dockerfile b/onlyoffice/tests/damengdb/damengdb.Dockerfile deleted file mode 100644 index f4f6cf1..0000000 --- a/onlyoffice/tests/damengdb/damengdb.Dockerfile +++ /dev/null @@ -1,57 +0,0 @@ -FROM onlyoffice/damengdb:8.1.2 as damengdb - -ARG DM8_USER="SYSDBA" -ARG DM8_PASS="SYSDBA001" -ARG DB_HOST="localhost" -ARG DB_PORT="5236" -ARG DISQL_BIN="/opt/dmdbms/bin" - -SHELL ["/bin/bash", "-c"] - -COPY <<"EOF" /wait_dm_ready.sh -#!/usr/bin/env bash - -function wait_dm_ready() { - cd /opt/dmdbms/bin - for i in `seq 1 10`; do - echo `./disql /nolog < /dev/null 2>&1 - if [ $? -eq 0 ]; then - echo "DM Database is not OK, please wait..." - sleep 10 - else - echo "DM Database is OK" - break - fi - done -} - -wait_dm_ready - -EOF - -COPY <<"EOF" /permissions.sql - -CREATE SYNONYM onlyoffice.DOC_CHANGES FOR sysdba.DOC_CHANGES; -CREATE SYNONYM onlyoffice.TASK_RESULT FOR sysdba.TASK_RESULT; -GRANT ALL PRIVILEGES ON sysdba.DOC_CHANGES TO onlyoffice; -GRANT ALL PRIVILEGES ON sysdba.TASK_RESULT TO onlyoffice; - -EOF - -RUN bash /opt/startup.sh > /dev/null 2>&1 \ - & mkdir -p /schema/damengdb \ - && apt update -y ; apt install wget -y \ - && wget https://raw.githubusercontent.com/ONLYOFFICE/server/master/schema/dameng/createdb.sql -P /schema/dameng/ \ - && bash ./wait_dm_ready.sh \ - && cd ${DISQL_BIN} \ - && ./disql $DM8_USER/$DM8_PASS@$DB_HOST:$DB_PORT -e \ - "create user "onlyoffice" identified by "onlyoffice" password_policy 0;" \ - && ./disql $DM8_USER/$DM8_PASS@$DB_HOST:$DB_PORT -e \ - "GRANT SELECT ON DBA_TAB_COLUMNS TO onlyoffice;" \ - && echo "EXIT" | tee -a /schema/dameng/createdb.sql \ - && ./disql $DM8_USER/$DM8_PASS@$DB_HOST:$DB_PORT \`/schema/dameng/createdb.sql \ - && ./disql $DM8_USER/$DM8_PASS@$DB_HOST:$DB_PORT \`/permissions.sql \ - && sleep 10 diff --git a/onlyoffice/tests/damengdb/docker-compose.yml b/onlyoffice/tests/damengdb/docker-compose.yml deleted file mode 100644 index c28dfb6..0000000 --- a/onlyoffice/tests/damengdb/docker-compose.yml +++ /dev/null @@ -1,67 +0,0 @@ -version: '2' -services: - onlyoffice-documentserver: - build: - context: ../../. - dockerfile: Dockerfile - target: documentserver - container_name: onlyoffice-documentserver - depends_on: - - onlyoffice-dameng - - onlyoffice-rabbitmq - environment: - - DB_TYPE=dameng - - DB_HOST=onlyoffice-dameng - - DB_PORT=5236 - - DB_NAME=onlyoffice - - DB_USER=onlyoffice - - AMQP_URI=amqp://guest:guest@onlyoffice-rabbitmq - # Costomize the JSON Web Token validation parameters if needed. - #- JWT_ENABLED=false - #- JWT_SECRET=secret - #- JWT_HEADER=Authorization - #- JWT_IN_BODY=true - ports: - - '80:80' - - '443:443' - stdin_open: true - restart: always - stop_grace_period: 60s - volumes: - - /var/www/onlyoffice/Data - - /var/log/onlyoffice - - /var/lib/onlyoffice/documentserver/App_Data/cache/files - - /var/www/onlyoffice/documentserver-example/public/files - - /usr/share/fonts - - onlyoffice-rabbitmq: - container_name: onlyoffice-rabbitmq - image: rabbitmq - restart: always - expose: - - '5672' - - onlyoffice-dameng: - container_name: onlyoffice-dameng - build: - context: . - dockerfile: damengdb.Dockerfile - target: damengdb - args: - DM8_USER: SYSDBA - DM8_PASS: SYSDBA001 - DB_HOST: localhost - DB_PORT: 5236 - environment: - - PAGE_SIZE=16 - - LD_LIBRARY_PATH=/opt/dmdbms/bin - - INSTANCE_NAME=dm8_01 - restart: always - expose: - - '5236' - volumes: - - dameng_data:/opt/dmdbms/data - -volumes: - dameng_data: - diff --git a/onlyoffice/tests/graphite.yml b/onlyoffice/tests/graphite.yml deleted file mode 100644 index 2bc4694..0000000 --- a/onlyoffice/tests/graphite.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../. - depends_on: - - onlyoffice-graphite - environment: - - METRICS_ENABLED=${METRICS_ENABLED:-true} - - METRICS_HOST=${METRICS_HOST:-localhost} - - METRICS_PORT=${METRICS_PORT:-8125} - - METRICS_PREFIX=${METRICS_PREFIX:-ds.} - stdin_open: true - restart: always - expose: - - '2003' - ports: - - '80:80' - volumes: - - ./graphite/statsd:/var/www/onlyoffice/documentserver/server/Metrics/config - - onlyoffice-graphite: - container_name: onlyoffice-graphite - image: graphiteapp/graphite-statsd - environment: - - GRAPHITE_STATSD_HOST=${GRAPHITE_STATSD_HOST:-onlyoffice-documentserver} - - GRAPHITE_TIME_ZONE=${GRAPHITE_TIME_ZONE:-Etc/UTC} - ports: - - '8888:80' - stdin_open: true - restart: always diff --git a/onlyoffice/tests/graphite/statsd/config.js b/onlyoffice/tests/graphite/statsd/config.js deleted file mode 100644 index 2ebffe6..0000000 --- a/onlyoffice/tests/graphite/statsd/config.js +++ /dev/null @@ -1,7 +0,0 @@ -{ - "graphiteHost": "onlyoffice-graphite", - "graphitePort": 2003, - "port": 8125, - "flushInterval": 60000, - "backends": [ "./backends/graphite.js" ] -} diff --git a/onlyoffice/tests/mariadb.yml b/onlyoffice/tests/mariadb.yml deleted file mode 100644 index 4bb8de5..0000000 --- a/onlyoffice/tests/mariadb.yml +++ /dev/null @@ -1,36 +0,0 @@ -version: '2.1' -services: - ds: - container_name: ds - build: - context: ../. - depends_on: - - onlyoffice-mariadb - environment: - - DB_TYPE=${DB_TYPE:-mysql} - - DB_HOST=${DB_HOST:-onlyoffice-mariadb} - - DB_PORT=${DB_PORT:-3306} - - DB_NAME=${DB_NAME:-onlyoffice} - - DB_USER=${DB_USER:-onlyoffice} - - DB_PWD=${DB_PWD:-onlyoffice} - stdin_open: true - restart: always - ports: - - '80:80' - - onlyoffice-mariadb: - container_name: onlyoffice-mariadb - image: mariadb:${MARIADB_VERSION:-10.5} - environment: - - MYSQL_DATABASE=${MYSQL_DATABASE:-onlyoffice} - - MYSQL_USER=${MYSQL_USER:-onlyoffice} - - MYSQL_PASSWORD=${MYSQL_PASSWORD:-onlyoffice} - - MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD:-yes} - restart: always - volumes: - - mysql_data:/var/lib/mysql - expose: - - '3306' - -volumes: - mysql_data: diff --git a/onlyoffice/tests/mssql/README.md b/onlyoffice/tests/mssql/README.md deleted file mode 100644 index 3db562f..0000000 --- a/onlyoffice/tests/mssql/README.md +++ /dev/null @@ -1,17 +0,0 @@ -## Stand Documentserver with mssql - -### How it works - -For deploy stand: - -**STEP 1**: Build you own images: - -```bash -sudo docker-compose build -``` - -**STEP 2**: Wait build complete and when: - -```bash -sudo docker-compose up -d -``` diff --git a/onlyoffice/tests/mssql/create_db_user.sh b/onlyoffice/tests/mssql/create_db_user.sh deleted file mode 100755 index cbe4c4e..0000000 --- a/onlyoffice/tests/mssql/create_db_user.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -#generate SA password -SYMBOLS='!#$%&*+,-.:;=?@^_~' -for (( i=1; i <= 20; i++ )); do - PASS=$(tr -dc "A-Za-z0-9$SYMBOLS" /dev/null | grep "affected" | wc -l) - if [ "$RES" -eq "1" ]; then - echo "Database is ready" - break - fi - sleep 10 -done - -#create new db user -$CONNECTION_STR "IF NOT EXISTS (SELECT * FROM sys.sql_logins WHERE name = '$MSSQL_USER') BEGIN CREATE LOGIN $MSSQL_USER WITH PASSWORD = '$MSSQL_PASSWORD' , CHECK_POLICY = OFF; ALTER SERVER ROLE [dbcreator] ADD MEMBER [$MSSQL_USER]; END" diff --git a/onlyoffice/tests/mssql/docker-compose.yml b/onlyoffice/tests/mssql/docker-compose.yml deleted file mode 100644 index 036bbe5..0000000 --- a/onlyoffice/tests/mssql/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../../. - dockerfile: Dockerfile - depends_on: - - onlyoffice-mssql - environment: - - DB_TYPE=${DB_TYPE:-mssql} - - DB_HOST=${DB_HOST:-onlyoffice-mssql} - - DB_PORT=${DB_PORT:-1433} - - DB_NAME=${DB_NAME:-onlyoffice} - - DB_USER=${DB_USER:-onlyoffice} - - DB_PWD=${DB_PWD:-onlyoffice} - stdin_open: true - restart: always - ports: - - '80:80' - - onlyoffice-mssql: - container_name: onlyoffice-mssql - build: - context: . - dockerfile: mssql.Dockerfile - args: - - MSSQL_DATABASE=${DB_NAME:-onlyoffice} - - MSSQL_USER=${DB_USER:-onlyoffice} - - MSSQL_PASSWORD=${DB_PWD:-onlyoffice} - restart: always - volumes: - - mssql_data:/var/opt/mssql - expose: - - '1433' - -volumes: - mssql_data: diff --git a/onlyoffice/tests/mssql/mssql.Dockerfile b/onlyoffice/tests/mssql/mssql.Dockerfile deleted file mode 100644 index f6dddc6..0000000 --- a/onlyoffice/tests/mssql/mssql.Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM mcr.microsoft.com/mssql/server:2022-latest as onlyoffice-mssql - -ENV ACCEPT_EULA=Y - -SHELL ["/bin/bash", "-c"] - -COPY create_db_user.sh /tmp/create_db_user.sh - -RUN bash /tmp/create_db_user.sh diff --git a/onlyoffice/tests/mysql.yml b/onlyoffice/tests/mysql.yml deleted file mode 100644 index 20fcd70..0000000 --- a/onlyoffice/tests/mysql.yml +++ /dev/null @@ -1,37 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../. - depends_on: - - onlyoffice-mysql - environment: - - DB_TYPE=${DB_TYPE:-mysql} - - DB_HOST=${DB_HOST:-onlyoffice-mysql} - - DB_PORT=${DB_PORT:-3306} - - DB_NAME=${DB_NAME:-onlyoffice} - - DB_USER=${DB_USER:-onlyoffice} - - DB_PWD=${DB_PWD:-onlyoffice} - stdin_open: true - restart: always - ports: - - '80:80' - - onlyoffice-mysql: - container_name: onlyoffice-mysql - image: mysql:${MYSQL_VERSION:-5.7} - command: --default-authentication-plugin=mysql_native_password - environment: - - MYSQL_DATABASE=${MYSQL_DATABASE:-onlyoffice} - - MYSQL_USER=${MYSQL_USER:-onlyoffice} - - MYSQL_PASSWORD=${MYSQL_PASSWORD:-onlyoffice} - - MYSQL_ALLOW_EMPTY_PASSWORD=${MYSQL_ALLOW_EMPTY_PASSWORD:-yes} - restart: always - volumes: - - mysql_data:/var/lib/mysql - expose: - - '3306' - -volumes: - mysql_data: diff --git a/onlyoffice/tests/oracle/README.md b/onlyoffice/tests/oracle/README.md deleted file mode 100644 index f548805..0000000 --- a/onlyoffice/tests/oracle/README.md +++ /dev/null @@ -1,17 +0,0 @@ -## Stand Documentserver with oracle - -### How it works - -For deploy stand: - -**STEP 1**: Build you own images: - -```bash -sudo docker-compose build -``` - -**STEP 2**: Wait build complete and when: - -```bash -sudo docker-compose up -d -``` diff --git a/onlyoffice/tests/oracle/create_db_user.sh b/onlyoffice/tests/oracle/create_db_user.sh deleted file mode 100755 index 831df42..0000000 --- a/onlyoffice/tests/oracle/create_db_user.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -CONNECTION_STR="sqlplus sys/$ORACLE_PASSWORD@//localhost:1521/$ORACLE_DATABASE as sysdba" - -export ORACLE_PWD=$ORACLE_PASSWORD - -#start db -/opt/oracle/runOracle.sh & - -#wait for db up -for (( i=1; i <= 20; i++ )); do - RES=$(echo "SELECT version FROM V\$INSTANCE;" | $CONNECTION_STR 2>/dev/null | grep "Connected" | wc -l) - if [ "$RES" -ne "0" ]; then - echo "Database is ready" - break - fi - sleep 10 -done - -sleep 1 - -#create new db user -$CONNECTION_STR < an unique provider name -- name: 'default-provider' - # org id. will default to orgId 1 if not specified - orgId: 1 - # name of the dashboard folder. Required - folder: dashboards - # folder UID. will be automatically generated if not specified - folderUid: '' - # provider type. Required - type: file - # disable dashboard deletion - disableDeletion: false - # enable dashboard editing - editable: true - # how often Grafana will scan for changed dashboards - updateIntervalSeconds: 10 - options: - # path to dashboard files on disk. Required - path: /opt/bitnami/grafana/dashboards - # enable folders creation for dashboards - #foldersFromFilesStructure: true diff --git a/onlyoffice/tests/prometheus/grafana/conf/prometheus.yml b/onlyoffice/tests/prometheus/grafana/conf/prometheus.yml deleted file mode 100644 index 11428bc..0000000 --- a/onlyoffice/tests/prometheus/grafana/conf/prometheus.yml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: 1 -datasources: - - name: Prometheus - type: prometheus - url: http://onlyoffice-prometheus:9090 - editable: true diff --git a/onlyoffice/tests/prometheus/grafana/dashboards/documentserver-statsd-exporter.json b/onlyoffice/tests/prometheus/grafana/dashboards/documentserver-statsd-exporter.json deleted file mode 100644 index d7a7c6f..0000000 --- a/onlyoffice/tests/prometheus/grafana/dashboards/documentserver-statsd-exporter.json +++ /dev/null @@ -1,2797 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "id": 8, - "links": [], - "panels": [ - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 22, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "none" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 0, - "y": 1 - }, - "hiddenSeries": false, - "id": 76, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_expireDoc_connections_edit", - "interval": "", - "legendFormat": "number of connections for editing", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Edit", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "none" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 1 - }, - "hiddenSeries": false, - "id": 78, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_expireDoc_connections_view", - "interval": "", - "legendFormat": "number of connections for viewing", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "View", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "none" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 1 - }, - "hiddenSeries": false, - "id": 80, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_expireDoc_connections_edit + ds_expireDoc_connections_view", - "interval": "", - "legendFormat": "sum of connections for editing and viewing", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Sum", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "none", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Сonnecting", - "type": "row" - }, - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 1 - }, - "id": 56, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 0, - "y": 2 - }, - "hiddenSeries": false, - "id": 52, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(ds_coauth_openDocument_open_sum[5m])/rate(ds_coauth_openDocument_open_count[5m])", - "interval": "", - "legendFormat": "moving average time of opening documents (for 5 minutes)", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Average time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 2 - }, - "hiddenSeries": false, - "id": 54, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_openDocument_open", - "interval": "", - "legendFormat": "quantile=\"{{quantile}}\"", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Quantile (0.5, 0.9, 0.99)", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "cpm" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 2 - }, - "hiddenSeries": false, - "id": 74, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_openDocument_open_count - ds_coauth_openDocument_open_count offset 1m", - "interval": "", - "legendFormat": "number of open documents", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "cpm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Opening Documents", - "type": "row" - }, - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 2 - }, - "id": 10, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "color": {}, - "custom": {}, - "thresholds": { - "mode": "absolute", - "steps": [] - }, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 0, - "y": 3 - }, - "hiddenSeries": false, - "id": 8, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(ds_conv_downloadFile_sum[5m])/rate(ds_conv_downloadFile_count[5m])", - "interval": "", - "legendFormat": "moving average time of downloading documents (for 5 minutes)", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Average time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 3 - }, - "hiddenSeries": false, - "id": 2, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_conv_downloadFile", - "interval": "", - "legendFormat": "quantile=\"{{quantile}}\"", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Quantile (0.5, 0.9, 0.99)", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "cpm" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 3 - }, - "hiddenSeries": false, - "id": 6, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_conv_downloadFile_count - ds_conv_downloadFile_count offset 1m", - "interval": "", - "legendFormat": "number of downloaded files", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "cpm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Downloading Documents", - "type": "row" - }, - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 3 - }, - "id": 12, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 0, - "y": 4 - }, - "hiddenSeries": false, - "id": 16, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(ds_conv_allconvert_sum[5m])/rate(ds_conv_allconvert_count[5m])", - "interval": "", - "legendFormat": "moving average time of converting documents (for 5 minutes)", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Average time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 4 - }, - "hiddenSeries": false, - "id": 18, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_conv_allconvert", - "interval": "", - "legendFormat": "quantile=\"{{quantile}}\"", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "quantile (0.5, 0.9, 0.99)", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "cpm" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 4 - }, - "hiddenSeries": false, - "id": 14, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_conv_allconvert_count - ds_conv_allconvert_count offset 1m", - "interval": "", - "legendFormat": "number of conversions", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "cpm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Converting Documents", - "type": "row" - }, - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 4 - }, - "id": 32, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 0, - "y": 5 - }, - "hiddenSeries": false, - "id": 30, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(ds_conv_spawnSync_sum[5m])/rate(ds_conv_spawnSync_count[5m])", - "interval": "", - "legendFormat": "moving average time of converting process (for 5 minutes)", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Average time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 5 - }, - "hiddenSeries": false, - "id": 28, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_conv_spawnSync", - "interval": "", - "legendFormat": "quantile=\"{{quantile}}\"", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Quantile (0.5, 0.9, 0.99)", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "cpm" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 5 - }, - "hiddenSeries": false, - "id": 26, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_conv_spawnSync_count - ds_conv_spawnSync_count offset 1m", - "interval": "", - "legendFormat": "number of conversion process", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "cpm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Converting Process", - "type": "row" - }, - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 5 - }, - "id": 48, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 0, - "y": 6 - }, - "hiddenSeries": false, - "id": 44, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(ds_coauth_data_auth_sum[5m])/rate(ds_coauth_data_auth_count[5m])", - "interval": "", - "legendFormat": "moving average time of completing authorization (for 5 minutes)", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Average time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 6 - }, - "hiddenSeries": false, - "id": 46, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_data_auth", - "interval": "", - "legendFormat": "quantile=\"{{quantile}}\"", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Quantile (0.5, 0.9, 0.99)", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "cpm" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 6 - }, - "hiddenSeries": false, - "id": 42, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_data_auth_count - ds_coauth_data_auth_count offset 1m", - "interval": "", - "legendFormat": "number of authorizations", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "cpm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Authorizations", - "type": "row" - }, - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 6 - }, - "id": 64, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 0, - "y": 7 - }, - "hiddenSeries": false, - "id": 60, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(ds_coauth_data_getLock_sum[5m])/rate(ds_coauth_data_getLock_count[5m])", - "interval": "", - "legendFormat": "moving average time of getLock duration (for 5 minutes)", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Average time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 7 - }, - "hiddenSeries": false, - "id": 62, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_data_getLock", - "interval": "", - "legendFormat": "quantile=\"{{quantile}}\"", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Quantile (0.5, 0.9, 0.99)", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "cpm" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 7 - }, - "hiddenSeries": false, - "id": 58, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_data_getLock_count - ds_coauth_data_getLock_count offset 1m", - "interval": "", - "legendFormat": "number of getLocks", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "cpm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Get Lock", - "type": "row" - }, - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 7 - }, - "id": 40, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 0, - "y": 8 - }, - "hiddenSeries": false, - "id": 36, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(ds_coauth_data_saveChanges_sum[5m])/rate(ds_coauth_data_saveChanges_count[5m])", - "interval": "", - "legendFormat": "moving average time of saving changes (for 5 minutes)", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Average time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 8 - }, - "hiddenSeries": false, - "id": 38, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_data_saveChanges", - "interval": "", - "legendFormat": "quantile=\"{{quantile}}\"", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Quantile (0.5, 0.9, 0.99)", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "cpm" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 8 - }, - "hiddenSeries": false, - "id": 34, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_data_saveChanges_count - ds_coauth_data_saveChanges_count offset 1m", - "interval": "", - "legendFormat": "number of saved changes ", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "cpm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Saving Changes", - "type": "row" - }, - { - "collapsed": true, - "datasource": null, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 8 - }, - "id": 72, - "panels": [ - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 0, - "y": 9 - }, - "hiddenSeries": false, - "id": 70, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "rate(ds_coauth_openDocument_imgurls_sum[5m])/rate(ds_coauth_openDocument_imgurls_count[5m])", - "interval": "", - "legendFormat": "moving average time to opening documents with uploading images (for 5 minutes)", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Average time", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "s" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 9 - }, - "hiddenSeries": false, - "id": 68, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_openDocument_imgurls", - "interval": "", - "legendFormat": "quantile=\"{{quantile}}\"", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Quantile (0.5, 0.9, 0.99)", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fieldConfig": { - "defaults": { - "custom": {}, - "unit": "cpm" - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 9 - }, - "hiddenSeries": false, - "id": 66, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.4.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "ds_coauth_openDocument_imgurls_count - ds_coauth_openDocument_imgurls_count offset 1m", - "interval": "", - "legendFormat": "the number of opened documents with the uploaded images", - "queryType": "randomWalk", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Count", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "cpm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "title": "Uploading Images", - "type": "row" - } - ], - "refresh": "30s", - "schemaVersion": 27, - "style": "dark", - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-3h", - "to": "now" - }, - "timepicker": {}, - "timezone": "", - "title": "Statsd DS", - "uid": "LDjoK2UGz", - "version": 24 -} \ No newline at end of file diff --git a/onlyoffice/tests/prometheus/prometheus-scrape/statsd-exporter.yml b/onlyoffice/tests/prometheus/prometheus-scrape/statsd-exporter.yml deleted file mode 100644 index b3322d0..0000000 --- a/onlyoffice/tests/prometheus/prometheus-scrape/statsd-exporter.yml +++ /dev/null @@ -1,6 +0,0 @@ -scrape_configs: - - job_name: 'statsd' - scrape_interval: 30s - static_configs: - - targets: - - onlyoffice-statsd-exporter:9102 diff --git a/onlyoffice/tests/rabbitmq-old.yml b/onlyoffice/tests/rabbitmq-old.yml deleted file mode 100644 index ce18691..0000000 --- a/onlyoffice/tests/rabbitmq-old.yml +++ /dev/null @@ -1,29 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../. - environment: - - AMQP_SERVER_TYPE=${AMQP_SERVER_TYPE:-rabbitmq} - - AMQP_SERVER_URL=${AMQP_SERVER_URL:-amqp://guest:guest@onlyoffice-rabbitmq} - stdin_open: true - restart: always - ports: - - '80:80' - - '443:443' - networks: - - onlyoffice - - onlyoffice-rabbitmq: - container_name: onlyoffice-rabbitmq - image: rabbitmq - restart: always - networks: - - onlyoffice - expose: - - '5672' - -networks: - onlyoffice: - driver: 'bridge' diff --git a/onlyoffice/tests/rabbitmq.yml b/onlyoffice/tests/rabbitmq.yml deleted file mode 100644 index 293045c..0000000 --- a/onlyoffice/tests/rabbitmq.yml +++ /dev/null @@ -1,29 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../. - environment: - - AMQP_TYPE=${AMQP_TYPE:-rabbitmq} - - AMQP_URI=${AMQP_URI:-amqp://guest:guest@onlyoffice-rabbitmq} - stdin_open: true - restart: always - ports: - - '80:80' - - '443:443' - networks: - - onlyoffice - - onlyoffice-rabbitmq: - container_name: onlyoffice-rabbitmq - image: rabbitmq:${RABBITMQ_VERSION:-latest} - restart: always - networks: - - onlyoffice - expose: - - '5672' - -networks: - onlyoffice: - driver: 'bridge' diff --git a/onlyoffice/tests/redis.yml b/onlyoffice/tests/redis.yml deleted file mode 100644 index 849be16..0000000 --- a/onlyoffice/tests/redis.yml +++ /dev/null @@ -1,31 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../. - args: - - PRODUCT_NAME=${PRODUCT_NAME:-documentserver} - environment: - - REDIS_SERVER_HOST=${REDIS_SERVER_HOST:-onlyoffice-redis} - - REDIS_SERVER_PORT=${REDIS_SERVER_PORT:-6379} - stdin_open: true - restart: always - ports: - - '80:80' - - '443:443' - networks: - - onlyoffice - - onlyoffice-redis: - container_name: onlyoffice-redis - image: redis:${REDIS_VERSION:-latest} - restart: always - networks: - - onlyoffice - expose: - - '6379' - -networks: - onlyoffice: - driver: 'bridge' diff --git a/onlyoffice/tests/standalone.yml b/onlyoffice/tests/standalone.yml deleted file mode 100644 index f3de32f..0000000 --- a/onlyoffice/tests/standalone.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: '2.1' -services: - onlyoffice-documentserver: - container_name: onlyoffice-documentserver - build: - context: ../. - args: - - PRODUCT_NAME=${PRODUCT_NAME:-documentserver} - stdin_open: true - restart: always - ports: - - '80:80' diff --git a/onlyoffice/tests/test.sh b/onlyoffice/tests/test.sh deleted file mode 100755 index 535003a..0000000 --- a/onlyoffice/tests/test.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -ssl=${ssl:-false} -private_key=${private_key:-tls.key} -certificate_request=${certificate_request:-tls.csr} -certificate=${certificate:-tls.crt} - -# Generate certificate -if [[ $ssl == "true" ]]; then - url=${url:-"https://localhost"} - - mkdir -p data/certs - pushd data/certs - - openssl genrsa -out ${private_key} 2048 - openssl req \ - -new \ - -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" \ - -key ${private_key} \ - -out ${certificate_request} - openssl x509 -req -days 365 -in ${certificate_request} -signkey ${private_key} -out ${certificate} - openssl dhparam -out dhparam.pem 2048 - chmod 400 ${private_key} - - popd -else - url=${url:-"http://localhost"} -fi - -# Check if the yml exists -if [[ ! -f $config ]]; then - echo "File $config doesn't exist!" - exit 1 -fi - -# Run test environment -docker-compose -p ds -f $config up -d - -wakeup_timeout=90 - -# Get documentserver healthcheck status -echo "Wait for service wake up" -sleep $wakeup_timeout -healthcheck_res=$(wget --no-check-certificate -qO - ${url}/healthcheck) - -# Fail if it isn't true -if [[ $healthcheck_res == "true" ]]; then - echo "Healthcheck passed." -else - echo "Healthcheck failed!" - exit 1 -fi - -docker-compose -p ds -f $config down diff --git a/override/core.example.env b/override/core.example.env index 8030ac3..dd03476 100644 --- a/override/core.example.env +++ b/override/core.example.env @@ -19,3 +19,12 @@ TRAEFIK_USERNAME= TRAEFIK_PASSWORD= TRAEFIK_EMAIL= TRAEFIK_HASHED_PASSWORD= + +# WireGuard VPN (SSH gateway) +# Public IP or DNS hostname of this server +WG_SERVERURL= +# Comma-separated peer names or a number (e.g. "laptop,phone" or "3") +# Each peer gets a config file + QR code in ./core/volumes/wireguard/config/ +WG_PEERS= +# Timezone for container (default: Europe/Berlin) +TZ=Europe/Berlin diff --git a/override/forgejo.example.env b/override/forgejo.example.env new file mode 100644 index 0000000..71facc7 --- /dev/null +++ b/override/forgejo.example.env @@ -0,0 +1,11 @@ +FORGEJO_DOMAIN=git.example.com +FORGEJO_VERSION=11 + +# Database (shared Postgres in core) +FORGEJO_DB_NAME=forgejo +FORGEJO_DB_USER=forgejo +FORGEJO_DB_PASSWORD= + +# Runner registration token (generate after Forgejo is up: +# docker exec -u 1000 forgejo forgejo actions generate-runner-token) +FORGEJO_RUNNER_TOKEN= diff --git a/override/gitlab.example.env b/override/gitlab.example.env deleted file mode 100644 index 89d8977..0000000 --- a/override/gitlab.example.env +++ /dev/null @@ -1 +0,0 @@ -GITLAB_DOMAIN= diff --git a/override/nextcloud.example.env b/override/nextcloud.example.env index 15a86a0..f1c0f25 100644 --- a/override/nextcloud.example.env +++ b/override/nextcloud.example.env @@ -8,3 +8,16 @@ NEXTCLOUD_DB_PASSWORD= NEXTCLOUD_DB_USER= NEXTCLOUD_TRUSTED_DOMAINS= ONLYOFFICE_JWT_SECRET= + +# Collabora Online +COLLABORA_DOMAIN= +COLLABORA_USERNAME=admin +COLLABORA_PASSWORD= + +# Nextcloud Talk High Performance Backend (HPB) +# Subdomain for the signaling server (e.g. signal.yourdomain.tld) +TALK_DOMAIN= +# Generate each secret with: openssl rand -hex 32 +TALK_TURN_SECRET= +TALK_SIGNALING_SECRET= +TALK_INTERNAL_SECRET= diff --git a/scripts/README-TLSA.md b/scripts/README-TLSA.md new file mode 100644 index 0000000..cd5dd16 --- /dev/null +++ b/scripts/README-TLSA.md @@ -0,0 +1,85 @@ +# TLSA Record Automation + +This directory contains scripts to automatically update TLSA/DANE records when certificates are renewed. + +## Setup + +### 1. Install Systemd Timer (Recommended) + +```bash +sudo cp /var/deploy/scripts/tlsa-timer.service /etc/systemd/system/ +sudo cp /var/deploy/scripts/tlsa-timer.timer /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable tlsa-timer.timer +sudo systemctl start tlsa-timer.timer +``` + +The timer will: +- Run 5 minutes after boot +- Check every hour for certificate changes +- Update TLSA records automatically when certificates change + +### 2. Manual Setup (Alternative) + +Add to crontab: +```bash +crontab -e +# Add this line: +0 * * * * /var/deploy/scripts/tlsa-monitor.sh >> /var/deploy/scripts/tlsa-cron.log 2>&1 +``` + +### 3. DNS Provider API Configuration (Optional) + +To enable automatic DNS updates via your DNS provider's API, set these environment variables: + +```bash +export DNS_API_KEY="your-api-key" +export DNS_API_PASSWORD="your-api-password" +export DNS_CUSTOMER_NUMBER="your-customer-number" +``` + +Or add to `/var/deploy/scripts/update-tlsa.sh`: +```bash +dnsApiKey="your-api-key" +dnsApiPassword="your-api-password" +dnsCustomerNumber="your-customer-number" +``` + +**Note:** DNS provider API integration is not yet fully implemented. Currently, the script logs the required TLSA values for manual DNS updates. You'll need to implement the API call for your specific DNS provider. + +## Manual TLSA Record Update + +If automation is not set up, run manually after certificate renewal: + +```bash +/var/deploy/scripts/update-tlsa.sh +``` + +This will output the TLSA records that need to be added/updated in your DNS provider. + +## Current TLSA Hash + +The current certificate hash is: +``` +5206c4482b4378bd3e86d22d7afd8f341eec95aa999aeff7d94454c197223418 +``` + +Add these TLSA records in your DNS provider: +- `_25._tcp.mail.nasarek.dev` → `3 1 1 5206c4482b4378bd3e86d22d7afd8f341eec95aa999aeff7d94454c197223418` +- `_465._tcp.mail.nasarek.dev` → `3 1 1 5206c4482b4378bd3e86d22d7afd8f341eec95aa999aeff7d94454c197223418` +- `_587._tcp.mail.nasarek.dev` → `3 1 1 5206c4482b4378bd3e86d22d7afd8f341eec95aa999aeff7d94454c197223418` + +## Verification + +Check if TLSA records are published: +```bash +dig +short TLSA _25._tcp.mail.nasarek.dev +dig +short TLSA _465._tcp.mail.nasarek.dev +dig +short TLSA _587._tcp.mail.nasarek.dev +``` + +## Troubleshooting + +- Check logs: `/var/deploy/scripts/tlsa-update.log` +- Test script manually: `/var/deploy/scripts/tlsa-monitor.sh` +- Verify certificate: `openssl x509 -in /var/deploy/mailcow/data/assets/ssl/cert.pem -noout -dates` diff --git a/scripts/secure-ssh-vpn.sh b/scripts/secure-ssh-vpn.sh new file mode 100644 index 0000000..7adf5e7 --- /dev/null +++ b/scripts/secure-ssh-vpn.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# Configure ufw firewall for this server stack: +# - SSH (22) : VPN clients only (10.13.13.0/24) +# - SMTP (25) : public (external mail servers must reach this) +# - HTTP/HTTPS : public (Traefik) +# - WireGuard UDP : public +# - Mail client ports (465, 587, 143, 993, 4190): public by default +# Set MAIL_VPN_ONLY=true to restrict these to VPN clients as well. +# +# Run this AFTER the WireGuard container is up and you have verified you can +# connect to the server via VPN (10.13.13.x). Once applied, SSH on port 22 is +# only reachable from within the WireGuard subnet. +# +# Usage: +# sudo bash scripts/secure-ssh-vpn.sh [--dry-run] [--mail-vpn-only] +# +# To undo SSH restriction: +# sudo ufw delete deny 22 +# sudo ufw delete allow from 10.13.13.0/24 to any port 22 + +set -euo pipefail + +WG_SUBNET="10.13.13.0/24" +SSH_PORT="22" +WG_UDP_PORT="51820" +DRY_RUN=false +MAIL_VPN_ONLY=false + +for arg in "$@"; do + [[ "$arg" == "--dry-run" ]] && DRY_RUN=true + [[ "$arg" == "--mail-vpn-only" ]] && MAIL_VPN_ONLY=true +done + +run() { + if $DRY_RUN; then + echo "[dry-run] $*" + else + "$@" + fi +} + +if [[ $EUID -ne 0 ]]; then + echo "Error: run this script as root (sudo)." >&2 + exit 1 +fi + +echo "=== WireGuard SSH firewall setup ===" +echo "VPN subnet : $WG_SUBNET" +echo "SSH port : $SSH_PORT (VPN only)" +echo "WG UDP port : $WG_UDP_PORT/udp" +echo "Mail VPN only : $MAIL_VPN_ONLY" +$DRY_RUN && echo "(dry-run mode — no changes will be made)" +echo + +# Ensure ufw is installed +if ! command -v ufw &>/dev/null; then + echo "Installing ufw..." + run apt-get install -y ufw +fi + +# ── Web & VPN ────────────────────────────────────────────────────────────── +run ufw allow 80/tcp comment 'HTTP (Traefik)' +run ufw allow 443/tcp comment 'HTTPS (Traefik)' +run ufw allow 2424/tcp comment 'Forgejo SSH (Traefik)' +run ufw allow "$WG_UDP_PORT/udp" comment 'WireGuard VPN' + +# ── Nextcloud Talk HPB (STUN/TURN) ──────────────────────────────────────── +run ufw allow 3478/tcp comment 'Nextcloud Talk STUN/TURN (TCP)' +run ufw allow 3478/udp comment 'Nextcloud Talk STUN/TURN (UDP)' + +# ── SSH — VPN clients only ──────────────────────────────────────────────── +# Deny rule is added first (gets higher rule number), then the VPN allow is +# inserted at position 1 so it always takes priority over the deny. +run ufw deny "$SSH_PORT/tcp" comment 'Block public SSH' +run ufw insert 1 allow from "$WG_SUBNET" to any port "$SSH_PORT" comment 'SSH via WireGuard VPN' + +# ── Mail (Mailcow) ──────────────────────────────────────────────────────── +# Port 25 must always be public so other mail servers can deliver to you. +run ufw allow 25/tcp comment 'SMTP (public — required for mail delivery)' + +if $MAIL_VPN_ONLY; then + # Restrict mail client access to VPN subnet only + for port in 465 587 143 993 4190; do + run ufw deny "$port/tcp" comment "Block public mail client port $port" + run ufw insert 1 allow from "$WG_SUBNET" to any port "$port" comment "Mail client port $port via VPN" + done + echo "Mail client ports (465, 587, 143, 993, 4190) restricted to VPN." +else + run ufw allow 465/tcp comment 'SMTPS (Mailcow)' + run ufw allow 587/tcp comment 'SMTP Submission (Mailcow)' + run ufw allow 143/tcp comment 'IMAP (Mailcow)' + run ufw allow 993/tcp comment 'IMAPS (Mailcow)' + run ufw allow 4190/tcp comment 'Sieve (Mailcow)' +fi + +# ── Enable / reload ufw ─────────────────────────────────────────────────── +if ! ufw status | grep -q "Status: active"; then + echo + echo "WARNING: enabling ufw. Make sure you are connected via WireGuard" + echo " or you will lose your current SSH session." + echo "Press Ctrl-C within 10 seconds to abort..." + sleep 10 + run ufw --force enable +else + run ufw reload +fi + +echo +echo "Done. Current ufw rules:" +ufw status numbered +echo +echo "To verify WireGuard is running:" +echo " docker compose -f /var/deploy/core/docker-compose.yml ps wireguard" +echo " docker exec wireguard wg show" +echo +echo "Client configs are in: ./core/volumes/wireguard/config/peer_/" diff --git a/scripts/tlsa-monitor.sh b/scripts/tlsa-monitor.sh new file mode 100755 index 0000000..9f564de --- /dev/null +++ b/scripts/tlsa-monitor.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Monitor certificate file and update TLSA records when it changes. +# This script checks the certificate modification time and updates TLSA if changed. + +set -e + +# Configuration. +certFile="/var/deploy/mailcow/data/assets/ssl/cert.pem" +stateFile="/var/deploy/scripts/.tlsa-state" +updateScript="/var/deploy/scripts/update-tlsa.sh" + +# Check if certificate file exists. +if [ ! -f "$certFile" ]; then + echo "Certificate file not found: $certFile" + exit 1 +fi + +# Get certificate modification time and hash. +currentMtime=$(stat -c %Y "$certFile" 2>/dev/null || stat -f %m "$certFile" 2>/dev/null) +currentHash=$(openssl x509 -in "$certFile" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':') + +# Read previous state. +if [ -f "$stateFile" ]; then + # shellcheck source=/dev/null + source "$stateFile" +else + previousMtime=0 + previousHash="" +fi + +# Check if certificate has changed. +if [ "$currentMtime" != "$previousMtime" ] || [ "$currentHash" != "$previousHash" ]; then + echo "Certificate changed detected. Updating TLSA records..." + + # Run update script. + if [ -x "$updateScript" ]; then + "$updateScript" + else + echo "ERROR: Update script not found or not executable: $updateScript" + exit 1 + fi + + # Save new state. + cat > "$stateFile" <