From e9091cbb8c2da4a5fb7d78df2d3672677f20ffb6 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 2 Nov 2022 10:32:56 +0100 Subject: [PATCH 001/170] [Rspamd] Update to 3.4 (fix of 3.3 Bug) --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 05d5d83b..828b81ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,7 +76,7 @@ services: - clamd rspamd-mailcow: - image: mailcow/rspamd:1.90 + image: mailcow/rspamd:1.91 stop_grace_period: 30s depends_on: - dovecot-mailcow From a9871d05b2349390e7ca98d477aaa95220d80c93 Mon Sep 17 00:00:00 2001 From: Vermium Sifell Date: Wed, 2 Nov 2022 23:42:37 +0100 Subject: [PATCH 002/170] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20Fixed=20invalid=20?= =?UTF-8?q?regexs=20for=20banning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/Dockerfiles/netfilter/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index 382a3f78..1ccc150e 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -97,9 +97,9 @@ def refreshF2bregex(): f2bregex[3] = 'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+' f2bregex[4] = 'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+' f2bregex[5] = 'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+' - f2bregex[6] = '-login: Disconnected \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' - f2bregex[7] = '-login: Aborted login \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' - f2bregex[8] = '-login: Aborted login \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' + f2bregex[6] = '-login: Disconnected.+ \(auth failed, .+\): user=.*, method=.+, rip=([0-9a-f\.:]+),' + f2bregex[7] = '-login: Aborted login.+ \(auth failed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' + f2bregex[8] = '-login: Aborted login.+ \(tried to use disallowed .+\): user=.+, rip=([0-9a-f\.:]+), lip.+' f2bregex[9] = 'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked' f2bregex[10] = '([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+' r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False)) From abfad4e0257c8865f1364843254992dee70468cf Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 3 Nov 2022 19:58:06 +0100 Subject: [PATCH 003/170] Add new action Check PRs if on staging --- .../assets/check_prs_if_on_staging.png | Bin 0 -> 72671 bytes .github/workflows/check_prs_if_on_staging.yml | 33 ++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 .github/workflows/assets/check_prs_if_on_staging.png create mode 100644 .github/workflows/check_prs_if_on_staging.yml diff --git a/.github/workflows/assets/check_prs_if_on_staging.png b/.github/workflows/assets/check_prs_if_on_staging.png new file mode 100644 index 0000000000000000000000000000000000000000..2e0fc7ff8793b23f3da9e37615d57f8b7825c430 GIT binary patch literal 72671 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 Z(%Hdo!NzYE`xp4{s;;3 Date: Sun, 6 Nov 2022 00:25:38 +0100 Subject: [PATCH 004/170] Add undocumented /api/v1/get/mailbox/all/domain.tld endpoint to documentation --- data/web/api/openapi.yaml | 54 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index 8fb6245c..c23380f1 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -5501,6 +5501,60 @@ paths: attr: spam_score: "8,15" summary: Edit mailbox spam filter score + "/api/v1/get/mailbox/all/{domain}": + get: + parameters: + - description: name of domain + in: path + name: domain + required: false + schema: + type: string + - description: e.g. api-key-string + example: api-key-string + in: header + name: X-API-Key + required: false + schema: + type: string + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + - active: "1" + attributes: + force_pw_update: "0" + mailbox_format: "maildir:" + quarantine_notification: never + sogo_access: "1" + tls_enforce_in: "0" + tls_enforce_out: "0" + domain: domain3.tld + is_relayed: 0 + local_part: info + max_new_quota: 10737418240 + messages: 0 + name: Full name + percent_class: success + percent_in_use: 0 + quota: 3221225472 + quota_used: 0 + rl: false + spam_aliases: 0 + username: info@domain3.tld + tags: ["tag1", "tag2"] + description: OK + headers: {} + tags: + - Mailboxes + description: You can list all mailboxes existing in system for a specific domain. + operationId: Get mailboxes of a domain + summary: Get mailboxes of a domain tags: - name: Domains From 4dd1b97e386a6f464396c41f56e0f4b3077adade Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Sun, 6 Nov 2022 15:52:30 +0100 Subject: [PATCH 005/170] [PHP] Update to 8.1 --- data/Dockerfiles/phpfpm/Dockerfile | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 74035c02..5360840a 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.0-fpm-alpine3.16 +FROM php:8.1-fpm-alpine3.16 LABEL maintainer "Andre Peters " ENV APCU_PECL 5.1.21 diff --git a/docker-compose.yml b/docker-compose.yml index 05d5d83b..02f92c3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -106,7 +106,7 @@ services: - rspamd php-fpm-mailcow: - image: mailcow/phpfpm:1.79 + image: mailcow/phpfpm:1.80 command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" depends_on: - redis-mailcow From df17e6b75e3ba3d6993932eff52e972406e60815 Mon Sep 17 00:00:00 2001 From: Nathaniel Mom Date: Sun, 6 Nov 2022 23:12:14 +1000 Subject: [PATCH 006/170] change 'return 1' to 'exit 1' --- helper-scripts/update_compose.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper-scripts/update_compose.sh b/helper-scripts/update_compose.sh index c58eabea..16769edc 100755 --- a/helper-scripts/update_compose.sh +++ b/helper-scripts/update_compose.sh @@ -50,7 +50,7 @@ echo -e "\e[32mTrying to determine GLIBC version...\e[0m" exit 0 else echo -e "\e[33mWARNING: $COMPOSE_PATH is not writable, but new version $LATEST_COMPOSE is available (installed: $COMPOSE_VERSION)\e[0m" - return 1 + exit 1 fi fi else From bc937ed2db47516fb68a4619fb14a14fbbad5368 Mon Sep 17 00:00:00 2001 From: Michael Cramer Date: Tue, 8 Nov 2022 09:45:25 +0100 Subject: [PATCH 007/170] [PHP] Polish dockerfile includes also #4839 because of --with-avif for gd configure command (is not available in 8.0) contains the following adjustments: - upgrade APCu to 5.1.22 - use PECL package for mailparse instead of git clone (3.1.4 is the latest one available and sice then no changes on master branch) - split PECL commands into separate ones (according to https://hub.docker.com/_/php this is the recommended way) - add missing configure options for gd extension to include webp, xpm and avif - specify composer version to be installed - cleanup more dev dependencies --- data/Dockerfiles/phpfpm/Dockerfile | 34 +++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 5360840a..38c68f70 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,12 +1,12 @@ FROM php:8.1-fpm-alpine3.16 LABEL maintainer "Andre Peters " -ENV APCU_PECL 5.1.21 +ENV APCU_PECL 5.1.22 ENV IMAGICK_PECL 3.7.0 -# Mailparse is pulled from master branch -#ENV MAILPARSE_PECL 3.0.2 +ENV MAILPARSE_PECL 3.1.4 ENV MEMCACHED_PECL 3.2.0 ENV REDIS_PECL 5.3.7 +ENV COMPOSER 2.4.4 RUN apk add -U --no-cache autoconf \ aspell-dev \ @@ -18,6 +18,7 @@ RUN apk add -U --no-cache autoconf \ freetype-dev \ g++ \ git \ + gettext \ gettext-dev \ gmp-dev \ gnupg \ @@ -27,8 +28,11 @@ RUN apk add -U --no-cache autoconf \ imagemagick-dev \ imap-dev \ jq \ + libavif \ + libavif-dev \ libjpeg-turbo \ libjpeg-turbo-dev \ + libmemcached \ libmemcached-dev \ libpng \ libpng-dev \ @@ -38,7 +42,9 @@ RUN apk add -U --no-cache autoconf \ libtool \ libwebp-dev \ libxml2-dev \ + libxpm \ libxpm-dev \ + libzip \ libzip-dev \ make \ mysql-client \ @@ -49,22 +55,24 @@ RUN apk add -U --no-cache autoconf \ samba-client \ zlib-dev \ tzdata \ - && git clone https://github.com/php/pecl-mail-mailparse \ - && cd pecl-mail-mailparse \ - && pecl install package.xml \ - && cd .. \ - && rm -r pecl-mail-mailparse \ - && pecl install redis-${REDIS_PECL} memcached-${MEMCACHED_PECL} APCu-${APCU_PECL} imagick-${IMAGICK_PECL} \ + && pecl install mailparse-${MAILPARSE_PECL} \ + && pecl install redis-${REDIS_PECL} \ + && pecl install memcached-${MEMCACHED_PECL} \ + && pecl install APCu-${APCU_PECL} \ + && pecl install imagick-${IMAGICK_PECL} \ && docker-php-ext-enable apcu imagick memcached mailparse redis \ && pecl clear-cache \ && docker-php-ext-configure intl \ && docker-php-ext-configure exif \ && docker-php-ext-configure gd --with-freetype=/usr/include/ \ --with-jpeg=/usr/include/ \ + --with-webp \ + --with-xpm \ + --with-avif \ && docker-php-ext-install -j 4 exif gd gettext intl ldap opcache pcntl pdo pdo_mysql pspell soap sockets zip bcmath gmp \ && docker-php-ext-configure imap --with-imap --with-imap-ssl \ && docker-php-ext-install -j 4 imap \ - && curl --silent --show-error https://getcomposer.org/installer | php \ + && curl --silent --show-error https://getcomposer.org/installer | php -- --version=${COMPOSER} \ && mv composer.phar /usr/local/bin/composer \ && chmod +x /usr/local/bin/composer \ && apk del --purge autoconf \ @@ -72,15 +80,21 @@ RUN apk add -U --no-cache autoconf \ cyrus-sasl-dev \ freetype-dev \ g++ \ + gettext-dev \ icu-dev \ imagemagick-dev \ imap-dev \ + libavif-dev \ libjpeg-turbo-dev \ + libmemcached-dev \ libpng-dev \ libressl-dev \ libwebp-dev \ libxml2-dev \ + libxpm-dev \ + libzip-dev \ make \ + openldap-dev \ pcre-dev \ zlib-dev From a6a7ab45f859a6b54652a4580dc63cb6561d7972 Mon Sep 17 00:00:00 2001 From: thomas Date: Sun, 13 Nov 2022 07:34:18 +0100 Subject: [PATCH 008/170] switch update.sh/check_online_status() from ping to curl to make it proxy ready --- update.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/update.sh b/update.sh index 707b22f5..69ada6b0 100755 --- a/update.sh +++ b/update.sh @@ -3,9 +3,9 @@ ############## Begin Function Section ############## check_online_status() { - CHECK_ONLINE_IPS=(1.1.1.1 9.9.9.9 8.8.8.8) - for ip in "${CHECK_ONLINE_IPS[@]}"; do - if timeout 3 ping -c 1 ${ip} > /dev/null; then + CHECK_ONLINE_DOMAINS=('https://github.com') + for domain in "${CHECK_ONLINE_DOMAINS[@]}"; do + if timeout 3 curl --head --silent --output /dev/null ${domain}; then return 0 fi done From b79a1530fb59ad35d00e05544e81429257b546b7 Mon Sep 17 00:00:00 2001 From: Link Steve <56674878+yhdsl@users.noreply.github.com> Date: Mon, 14 Nov 2022 21:18:37 +0800 Subject: [PATCH 009/170] Update Simplified Chinese Translation --- data/web/lang/lang.zh-cn.json | 990 +++++++++++++++++++--------------- 1 file changed, 568 insertions(+), 422 deletions(-) diff --git a/data/web/lang/lang.zh-cn.json b/data/web/lang/lang.zh-cn.json index bbe5c7c3..2b5685ee 100644 --- a/data/web/lang/lang.zh-cn.json +++ b/data/web/lang/lang.zh-cn.json @@ -2,100 +2,106 @@ "acl": { "alias_domains": "添加域名别名", "app_passwds": "管理应用密码", - "bcc_maps": "BCC映射", - "delimiter_action": "邮件地址标签处理", - "eas_reset": "重置Exchange ActiveSync设备", - "extend_sender_acl": "允许用外部地址扩展访问控制表", + "bcc_maps": "BCC 映射", + "delimiter_action": "邮箱地址标签处理", + "domain_desc": "更改域名描述", + "domain_relayhost": "更改域名的中继主机", + "eas_reset": "重置 Exchange ActiveSync (EAS) 设备", + "extend_sender_acl": "允许通过外部地址扩展发件人的 ACL 设置", "filters": "过滤器", - "login_as": "以邮箱用户登录", - "prohibited": "禁止访问", + "login_as": "作为邮箱用户登录", + "mailbox_relayhost": "更改邮箱的中继主机", + "prohibited": "被 ACL 禁止访问", "protocol_access": "更改访问协议", "pushover": "Pushover", "quarantine": "隔离操作", "quarantine_attachments": "隔离附件", + "quarantine_category": "更改隔离通知类型", "quarantine_notification": "更改隔离通知", "ratelimit": "频率限制", "recipient_maps": "收件人映射", - "smtp_ip_access": "更改SMTP允许主机", - "sogo_access": "允许管理SOGo访问", - "sogo_profile_reset": "重置SOGo个人资料", + "smtp_ip_access": "更改 SMTP 允许主机", + "sogo_access": "允许管理 SOGo 访问权限", + "sogo_profile_reset": "重置 SOGo 个人资料", "spam_alias": "临时别名", "spam_policy": "黑名单/白名单", "spam_score": "垃圾邮件分数", "syncjobs": "同步任务", - "tls_policy": "TLS策略", - "unlimited_quota": "无限邮箱容量配额", - "domain_desc": "更改域名描述" + "tls_policy": "TLS 策略", + "unlimited_quota": "无限邮箱容量配额" }, "add": { - "activate_filter_warn": "当\"启用\"被勾选,所有其他过滤器都会被禁用", + "activate_filter_warn": "当 \"启用\" 选项被勾选后,所有其他过滤器都会被禁用", "active": "启用", "add": "添加", "add_domain_only": "只添加域名", "add_domain_restart": "添加域名并重启", "alias_address": "地址别名", - "alias_address_info": "完整邮件地址,或 @example.com 以捕获此域名下所有邮件地址的信息 (英文逗号分隔多个地址)。 只允许此mailcow实例下的域名", + "alias_address_info": "使用完整的邮箱地址,或使用 @example.com 匹配此域名下所有的邮箱地址 (存在多个地址时使用英文逗号分隔)。 只允许此 Mailcow 实例下的域名", "alias_domain": "域名别名", - "alias_domain_info": "只允许合法的域名 (英文逗号分隔多个地址)", + "alias_domain_info": "只允许合法的域名 (存在多个地址时使用英文逗号分隔)", "app_name": "应用名称", "app_password": "添加应用密码", - "automap": "尝试自动映射文件夹 (如:\"已发送\", \"Sent\" => \"Sent\")", + "app_passwd_protocols": "应用密码允许的协议", + "automap": "将尝试自动映射文件夹 (例如将 \"已发送消息\" 和 \"已发送\" 均映射到 \"已发送\" 文件夹)", "backup_mx_options": "中继选项", - "comment_info": "私密评论对用户不可见,公开评论会给用户展示为鼠标悬停显示的提示", + "bcc_dest_format": "BCC 的目标地址必须是一个有效的电子邮箱地址。
如果你需要向多个地址发送副本,请创建一个别名并在此使用。", + "comment_info": "私密评论对用户不可见,公开评论将会在鼠标悬停时作为用户提示显示", "custom_params": "自定义参数", - "custom_params_hint": "正确的写法: --param=xy ,错误的写法: --param xy", - "delete1": "完成后将源邮件删除", - "delete2": "删除目的邮箱中存在但源邮箱中不存在的邮件", - "delete2duplicates": "删除目的邮箱中的重复邮件", + "custom_params_hint": "正确的格式: --param=xy ,错误的格式: --param xy", + "delete1": "在完成后删除源邮件", + "delete2": "删除在目标邮箱中存在但在源邮箱中不存在的邮件", + "delete2duplicates": "删除目标邮箱中的重复邮件", "description": "描述", - "destination": "目的邮箱", - "disable_login": "不允许登录 (仍然会接收邮件)", + "destination": "目标邮箱", + "disable_login": "不允许登录 (但仍然会接收邮件)", "domain": "域名", - "domain_matches_hostname": "域名 %s 与主机名匹配", + "domain_matches_hostname": "域名 %s 与主机名称匹配", "domain_quota_m": "域名总配额 (MiB)", "enc_method": "加密方法", - "exclude": "拒绝对象 (regex)", - "full_name": "全名", + "exclude": "拒绝对象 (Regex)", + "full_name": "全称", "gal": "全球地址簿", - "gal_info": "全球地址簿包含了域名下的所有对象,并且此行为不能被用户更改。如果关闭,用户的 空闲/繁忙 信息将不能在SOGo中显示。 重启SOGo以应用更改。", + "gal_info": "全球地址簿包含了域名下的所有对象,并且此行为不能被用户更改。如果关闭,用户的 \"空闲/繁忙\" 的状态将无法在 SOGo 中显示。 重启 SOGo 服务以应用更改。", "generate": "生成", - "goto_ham": "学习为 非垃圾邮件", + "goto_ham": "学习为非垃圾邮件", "goto_null": "静默丢弃邮件", - "goto_spam": "学习为 垃圾邮件", + "goto_spam": "学习为垃圾邮件", "hostname": "主机名", "inactive": "禁用", "kind": "类型", "mailbox_quota_def": "默认邮箱配额", "mailbox_quota_m": "每个邮箱的最大配额 (MiB)", - "mailbox_username": "用户名
(邮件地址的左侧部分)", - "max_aliases": "最大允许地址别名数", - "max_mailboxes": "最大允许邮箱数", + "mailbox_username": "用户名 (邮箱地址左侧的部分)", + "max_aliases": "最大允许的地址别名数", + "max_mailboxes": "最大允许的邮箱数", "mins_interval": "轮询间隔 (分钟)", "multiple_bookings": "登记限制", "nexthop": "下一跳", "password": "密码", - "password_repeat": "确认密码
(重复输入)", + "password_repeat": "确认密码 (重复)", "port": "端口", - "post_domain_add": "在添加新域名后SOGo的容器\"sogo-mailcow\"需要重新启动。

此外请检查并修改域名的DNS配置。一旦DNS配置生效, 重启\"acme-mailcow\"容器以自动地为你的新域名生成证书 (autoconfig.<domain>, autodiscover.<domain>)。
重新启动容器是可选的,生成证书的操作会每24小时重试一次。", + "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_all_info": "↪ 如果选择中继所有收件人,你将需要为每个需要中继的邮件添加一个(\"虚拟\")邮箱。", "relay_domain": "中继这个域名", - "relay_transport_info": "
你可以为此域名定义传输规则以自定义发件目标主机,否则遵照MX记录发送邮件。", - "relay_unknown_only": "只为不存在的邮箱地址中继。已存在的邮箱地址则在本地递送。", - "relayhost_wrapped_tls_info": "请 不要 使用\"嵌套TLS\"的端口 (大多为端口465).
\r\n使用其他\"非嵌套\"的端口发起STARTTLS. 你可以在\"TLS策略规则\"中添加强制使用TLS的策略。", + "relay_transport_info": "
注意
你可以为此域名自定义传输规则来指定发件目标主机,否则将按照 MX 记录发送邮件。", + "relay_unknown_only": "只为不存在的邮箱地址中继。已存在的邮箱地址将在本地发送。", + "relayhost_wrapped_tls_info": "请不要使用\"嵌套 TLS \"的端口 (大多数为端口 465)。
\r\n使用其他\"非嵌套\"的端口发起 STARTTLS。你可以在\"TLS 策略规则\"中添加强制使用 TLS 的策略。", "select": "请选择...", "select_domain": "请先选择一个域名", "sieve_desc": "简短描述", "sieve_type": "过滤器类型", - "skipcrossduplicates": "跳过其他文件夹中已存在的邮件(保留先存在的邮件)", + "skipcrossduplicates": "跳过其他文件夹中已存在的邮件 (保留已经存在的邮件)", "subscribeall": "订阅所有文件夹", "syncjob": "添加同步任务", - "syncjob_hint": "注意密码需要以明文存储!", + "syncjob_hint": "注意密码将以明文存储!", + "tags": "标签", "target_address": "目标地址", - "target_address_info": "完整的邮箱地址 (英文逗号分隔多个地址)。", + "target_address_info": "完整的邮箱地址 (存在多个地址时使用英文逗号分隔)", "target_domain": "目标域名", "timeout1": "远程主机连接超时时间", "timeout2": "本地主机连接超时时间", @@ -104,12 +110,12 @@ "validation_success": "验证成功" }, "admin": { - "access": "访问", + "access": "权限管理", "action": "操作", - "activate_api": "启用API", + "activate_api": "启用 API", "activate_send": "启用发送按钮", "active": "启用", - "active_rspamd_settings_map": "启用的设置规则", + "active_rspamd_settings_map": "启用的规则", "add": "添加", "add_admin": "添加管理员", "add_domain_admin": "添加域名管理员", @@ -124,36 +130,41 @@ "admin": "管理员", "admin_details": "编辑管理员详情", "admin_domains": "分配域名", + "admins": "管理员", + "admins_ldap": "LDAP 管理员", "advanced_settings": "高级设置", - "api_allow_from": "允许来自这些IP/CIDR网络的API访问", - "api_info": "API功能仍在完善中。你可以在/api找到API文档", - "api_key": "API密钥", - "api_skip_ip_check": "跳过API的IP检查", + "api_allow_from": "允许来自这些 IP/CIDR 网络的 API 访问", + "api_info": "API 功能仍在完善中。你可以在这里找到 API 文档", + "api_key": "API 密钥", + "api_read_only": "只读权限", + "api_read_write": "可写权限", + "api_skip_ip_check": "跳过 API 的 IP 检查", "app_links": "应用链接", "app_name": "应用名称", - "apps_name": "\"mailcow Apps\" 名称", - "arrival_time": "到达时间(服务器)", + "apps_name": "Mailcow 应用的名称", + "arrival_time": "到达时间 (服务器时间)", "authed_user": "已认证用户", "ays": "确定继续操作?", - "ban_list_info": "下为封禁掉的IP列表: 网络 (剩余封禁时间) - [操作]
取消封禁的IP将会在几秒之内从封禁列表中移除
红色标签表示因黑名单而导致的永久封禁", - "change_logo": "更改logo", + "ban_list_info": "以下为被封禁的 IP 列表: 网络 (剩余封禁时间) - [操作]
被取消封禁的 IP 将会在几秒之内从封禁列表中移除
红色标签表示因黑名单而导致的永久封禁", + "change_logo": "更改 Logo", "configuration": "配置", - "credentials_transport_warning": "警告: 添加新的传输规则会为所有\"下一跳\"列匹配的规则更新认证凭证。", - "customer_id": "客户ID", - "customize": "自定义", + "convert_html_to_text": "将 HTML 转换为纯文本内容", + "credentials_transport_warning": "警告: 添加新的传输规则将会为所有的\"下一跳\"列匹配的规则更新认证凭证。", + "customer_id": "客户 ID", + "customize": "页面自定义", "delete_queue": "删除所有", "destination": "目标地址", - "dkim_add_key": "添加ARC/DKIM密钥", + "dkim_add_key": "添加 ARC/DKIM 密钥", "dkim_domains_selector": "选择器", - "dkim_domains_wo_keys": "选择没有密钥的域名", + "dkim_domains_wo_keys": "选择缺失密钥的域名", "dkim_from": "从", "dkim_from_title": "源域名 - 数据来源", - "dkim_key_length": "DKIM密钥长度 (bits)", + "dkim_key_length": "DKIM 密钥长度 (Bits)", "dkim_key_missing": "密钥缺失", - "dkim_key_unused": "密钥未被使用", + "dkim_key_unused": "密钥闲置", "dkim_key_valid": "密钥合法", - "dkim_keys": "ARC/DKIM密钥", - "dkim_overwrite_key": "覆盖已存在的DKIM密钥", + "dkim_keys": "ARC/DKIM 密钥", + "dkim_overwrite_key": "覆盖已存在的 DKIM 密钥", "dkim_private_key": "私钥", "dkim_to": "到", "dkim_to_title": "目标域名 - 数据将会被覆盖", @@ -162,48 +173,51 @@ "domain_admins": "域名管理员", "domain_s": "域名", "duplicate": "复制", - "duplicate_dkim": "复制DKIM记录", + "duplicate_dkim": "复制 DKIM 记录", "edit": "编辑", "empty": "结果为空", "excludes": "除了", "f2b_ban_time": "封禁时间 (秒)", "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_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_table": "筛选表格", "flush_queue": "清空队列", "forwarding_hosts": "转发主机", - "forwarding_hosts_add_hint": "你可以指定 IPv4/IPv6 地址、CIDR 表示的网络、主机名 (解析为IP地址),或者邮箱域名 (查询SPF记录或MX记录并解析为IP地址)。", - "forwarding_hosts_hint": "来自此处所列的主机的入站信息会被无条件接收。并且这些主机不会经过DNSBL检查或者被加入灰名单。 来自它们的垃圾邮件不会被拒绝,但可选的可以被移入垃圾文件夹。当你设置了转发规则将邮件转发到此mailcow服务器,通常你可以将来源主机加入此列表。", + "forwarding_hosts_add_hint": "你可以指定 IPv4/IPv6 地址、CIDR 表示的网络、主机名 (解析为 IP 地址),或者邮箱域名 (查询 SPF 记录或 MX 记录并解析为 IP 地址)。", + "forwarding_hosts_hint": "来自此处所列的主机的入站信息将会被无条件接收。并且这些主机将不会经过 DNSBL 检查或者被加入灰名单。来自它们的垃圾邮件将不会被拒绝,但可以选择将其移入垃圾文件夹。当你设置了转发规则将邮件转发到此 Mailcow 服务器,你通常可以将来源主机加入此列表。", "from": "来自", "generate": "生成", - "guid": "GUID - 唯一实例ID", + "guid": "GUID - 唯一的安装实例 ID", "guid_and_license": "GUID和许可证", - "hash_remove_info": "移除一个频率限制特征 (如果还存在的话) 会完全移除它的计数器。
\r\n 每个特征将以不同颜色表示。", - "help_text": "覆盖登录面板下的帮助文字 (允许使用HTML)", + "hash_remove_info": "移除一个频率限制特征 (如果还存在的话) 将会完全移除它的计数器。
\r\n 每个特征将以不同颜色表示。", + "help_text": "覆盖登录面板下的帮助文字 (允许使用 HTML)", "host": "主机", + "html": "HTML", "import": "导入", "import_private_key": "导入私钥", "in_use_by": "使用者", "inactive": "禁用", "include_exclude": "包括/排除", - "include_exclude_info": "没有选择时默认包括所有邮箱", + "include_exclude_info": "默认 - 没有选择时默认包括所有邮箱", "includes": "包括这些收件人", + "is_mx_based": "基于 MX 记录", "last_applied": "最后应用的条目", - "license_info": "你不需要获取证书以使用此项目,但是获取证书可以帮助此项目进一步发展。
在这里注册你的GUID 或者 为你的mailcow安装购买支持服务。", + "license_info": "你不需要获取证书便可以使用此项目,但是获取证书可以帮助此项目进一步发展。
在这里注册你的 GUID或者为你的 Mailcow 安装购买支持服务。", "link": "链接", "loading": "请等待...", - "logo_info": "你的图片会在顶部导航栏被缩放为40px高,在起始页被缩放为最大250px高度。强烈推荐使用能较好缩放的图片。", - "lookup_mx": "匹配MX记录 (如匹配.outlook.com MX记录以通过这一跳来路由所有指向*.outlook.com的邮件)", - "main_name": "\"mailcow UI\" 名称", - "merged_vars_hint": "灰色行来自 vars.(local.)inc.php 并且不能被更改。", + "login_time": "登录时间", + "logo_info": "你的图片将会在顶部导航栏被缩放为 40px 高,在起始页被缩放为最大 250px 高。强烈推荐使用能较好适应缩放的图片。", + "lookup_mx": "应当为一个正则表达式,用于匹配 MX 记录 (例如 .*google\\.com 将转发所有拥有以 google.com 结尾的 MX 记录的邮件)", + "main_name": "Mailcow UI 的名称", + "merged_vars_hint": "灰色行来自 vars.(local.)inc.php 文件并且无法修改。", "message": "消息", "message_size": "消息大小", "nexthop": "下一跳", @@ -211,58 +225,69 @@ "no_active_bans": "没有启用的封禁", "no_new_rows": "已经到底了", "no_record": "没有记录", - "oauth2_client_id": "客户端ID", - "oauth2_client_secret": "客户端secret", - "oauth2_info": "此OAuth2实现支持\"Authorization Code\"和生成refresh token。
\r\n并且服务器会自动在refresh token被使用后重新生成refresh token。

\r\n→ 默认的scope是 profile。只有邮箱用户可以使用OAuth2来认证。如果scope参数被省略则会回退到 profile
\r\n→ 客户端必须在认证请求中发送 state 参数。

\r\nOAuth2 API路径:
\r\n
    \r\n
  • Authorization endpoint: /oauth/authorize
  • \r\n
  • Token endpoint: /oauth/token
  • \r\n
  • Resource page: /oauth/profile
  • \r\n
\r\n重新生成客户端secret不会使已存在的authorization code过期,但是会在它们刷新token时失败。

\r\n撤销客户端token会立即终止所有活动会话。 所有的客户端都需要重新认证。", - "oauth2_redirect_uri": "重定向URI", - "oauth2_renew_secret": "生成新的客户端secret", - "oauth2_revoke_tokens": "撤销所有客户端token", + "oauth2_apps": "OAuth2 应用", + "oauth2_add_client": "添加 OAuth2 客户端", + "oauth2_client_id": "客户端 ID", + "oauth2_client_secret": "客户端 secret", + "oauth2_info": "此 OAuth2 服务支持 \"Authorization Code\" 和生成 refresh token。
\r\n并且服务器会自动在 refresh token 被使用后重新生成新的 refresh token。

\r\n→ 默认的 scope 是 profile。只有邮箱用户可以使用 OAuth2 进行认证。如果 scope 参数被省略则会回退到 profile
\r\n→ 客户端必须在认证请求中发送 state 参数。

\r\nOAuth2 API 路径:
\r\n
    \r\n
  • Authorization endpoint: /oauth/authorize
  • \r\n
  • Token endpoint: /oauth/token
  • \r\n
  • Resource page: /oauth/profile
  • \r\n
\r\n重新生成客户端 secret 不会使已存在的 authorization code 过期,但是会在使用它们刷新 token 时失败。

\r\n撤销客户端的 token 会立即终止所有的活动会话。 并且所有的客户端都需要重新认证。", + "oauth2_redirect_uri": "重定向 URI", + "oauth2_renew_secret": "生成新的客户端 secret", + "oauth2_revoke_tokens": "撤销所有的客户端 token", + "optional": "可选", "password": "密码", + "password_length": "密码长度", + "password_policy": "密码规则", + "password_policy_chars": "必须包含至少一个英文字母", + "password_policy_length": "密码最小长度为 %d", + "password_policy_lowerupper": "必须包含小写和大写的英文字母", + "password_policy_numbers": "必须包含至少一个数字", + "password_policy_special_chars": "必须包含特殊字符", "password_repeat": "确认密码 (重复)", "priority": "优先级", "private_key": "私钥", "quarantine": "隔离", - "quarantine_bcc": "发送所有通知邮件的副本(BCC)到这个收件人:
留空以关闭。 发送的邮件未被签名也未被检查。 故只应在内部递送。", - "quarantine_exclude_domains": "不启用隔离的域名和域名别名", - "quarantine_max_age": "最长保留日数
必须大于或等于1日", - "quarantine_max_size": "最大文件大小,单位MiB (超出限制的元素会被丢弃):
0 表示不限大小。", - "quarantine_max_score": "如果垃圾分数大于此值则不通知:
默认为 9999.0", - "quarantine_notification_html": "通知邮件模板:
留空以恢复默认模板。", + "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_redirect": "转发所有的通知到这个收件人:
留空以禁用该操作。 发送的邮件未被签名也未被检查。 故只应在内部传递。", + "quarantine_release_format": "被移除的项目的格式", "quarantine_release_format_att": "附件", - "quarantine_release_format_raw": "未修改原件", + "quarantine_release_format_raw": "原件 (未修改)", "quarantine_retention_size": "每个邮箱保留隔离项目数:
0 表示 禁用", - "queue_ays": "请确认你真的想要删除当前队列中的所有项目。", - "queue_deliver_mail": "递送", + "queue_ays": "你真的确定想要删除当前队列中的所有项目吗?", + "queue_deliver_mail": "传递", "queue_hold_mail": "暂停", - "queue_manager": "队列管理器", + "queue_manager": "队列管理", + "queue_show_message": "显示消息", "queue_unban": "队列取消封禁", "queue_unhold_mail": "继续", - "queue_show_message": "显示消息", - "quota_notification_html": "通知邮件模板:
留空以恢复默认模板。", + "quota_notification_html": "通知邮件模板:
留空则恢复默认模板。", "quota_notification_sender": "通知邮件发件人", "quota_notification_subject": "通知邮件主题", "quota_notifications": "配額通知", - "quota_notifications_info": "配額通知会在用户配额超过80%和95%时各发送一次。", - "quota_notifications_vars": "{{percent}} 表示当前用户配额
{{username}} 未邮箱名称", + "quota_notifications_info": "配額通知会在用户配额超过 80% 和 95% 时各发送一次。", + "quota_notifications_vars": "{{percent}} 表示当前用户配额
{{username}} 为邮箱名称", "r_active": "启用的限制规则", "r_inactive": "禁用的限制规则", - "r_info": "启用的限制规则列表中灰色的/关闭的元素不能被mailcow识别为合法的限制规则,且不可移动。未知的限制规则将会按照原来的顺序被设置。
你可以在 inc/vars.local.inc.php 中添加新元素以勾选它们。", + "r_info": "启用的限制规则列表中灰色的和关闭的部分无法被 Mailcow 识别为合法的限制规则,并且不可移动。未知的限制规则将会按照原来的顺序被设置。
你可以在 inc/vars.local.inc.php 中添加新内容以勾选它们。", "rate_name": "频率名称", "recipients": "收件人", "refresh": "刷新", - "regen_api_key": "重新生成API密钥", + "regen_api_key": "重新生成 API 密钥", "regex_maps": "正则表达式规则", "relay_from": "\"来自:\" 地址", + "relay_rcpt": "\"到达:\" 地址", "relay_run": "运行测试", "relayhosts": "中继传输", - "relayhosts_hint": "定义的中继传输可以在域名配置弹出框中被选择。
\r\n 中继传输服务总是使用 \"smtp:\" 并且会在可能时使用STARTTLS。不支持SMTPS。用户的出站TLS策略会影响此行为。
\r\n 对选中的域名和域名别名生效。", + "relayhosts_hint": "自定义的中继传输可以在域名配置的弹出框中被选择。
\r\n 中继传输服务总是使用 \"smtp:\" 并且会在可能时使用 TLS,并且不支持 Wrapped TLS (SMTPS)。用户的出站 TLS 策略将会影响此行为。
\r\n 对选中的域名和域名别名生效。", "remove": "删除", "remove_row": "删除行", - "reset_default": "重置回默认值", + "reset_default": "重置为默认值", "reset_limit": "移除特征", "routing": "路由", "rsetting_add_rule": "添加规则", @@ -271,221 +296,237 @@ "rsetting_no_selection": "请选择一个规则", "rsetting_none": "没有可用的规则", "rsettings_insert_preset": "插入示例预设 \"%s\"", - "rsettings_preset_1": "为已认证用户关闭除DKIM和ratelimit规则外的所有规则", - "rsettings_preset_2": "管理员(postmaster)想要垃圾邮件", - "rsettings_preset_3": "只允许指定的发件人 (如只允许内部邮箱发送)", - "rspamd_com_settings": "自动生成设置名称,请看下方的示例预设。查看Rspamd docs以了解更多细节。", + "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等级", + "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_id": "服务ID", + "service": "服务", + "service_id": "服务 ID", "source": "来源", "spamfilter": "垃圾邮件过滤器", "subject": "主题", + "success": "成功", "sys_mails": "系统邮件", "text": "文本", "time": "时间", "title": "标题", - "title_name": "\"mailcow UI\" 网站标题", + "title_name": "Mailcow UI 的网站标题", "to_top": "返回顶部", - "transport_dest_format": "格式: example.org, .example.org, *, box@example.org (英文逗号分隔多个值)", + "transport_dest_format": "正则表达式格式: example.org, .example.org, *, box@example.org (存在多个地址时使用英文逗号分隔)", "transport_maps": "传输规则", - "transports_hint": "→ 传输规则条目优先于中继传输

\r\n→ 用户的出站TLS策略设置会被忽略,只会执行域名的TLS策略规则。
\r\n→ 传输服务总是使用 \"smtp:\" 并且会在可能时使用STARTTLS。不支持SMTPS。\r\n→ 匹配 \"/localhost$/\" 的地址会通过 \"local:\" 传输,但是 \"*\" 不会匹配这些本地地址。
\r\n→ 为了确定下一跳 \"[host]:25\" 的认证凭证, Postfix 总会 先查询 \"host\" 而不是 \"[host]:25\"。此行为使不能同时使用 \"host\" 和 \"[host]:25\"。", - "ui_footer": "页脚 (允许使用HTML)", + "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_content": "文本 (允许使用 HTML)", + "ui_header_announcement_help": "公告会在登录页面以及用户登录后的页面显示。", "ui_header_announcement_select": "选择公告类型", "ui_header_announcement_type": "类型", - "ui_header_announcement_type_info": "信息", - "ui_header_announcement_type_warning": "重要", - "ui_header_announcement_type_danger": "非常重要", - "ui_texts": "UI标签和文本", + "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": "用户名", - "validate_license_now": "通过证书服务器验证GUID", + "validate_license_now": "通过证书服务器验证 GUID", "verify": "验证", "yes": "✓" }, "danger": { - "access_denied": "访问拒绝或表单数据非法", - "alias_domain_invalid": "域名别名 %s 非法", + "access_denied": "访问被拒绝或者表单数据无效", + "alias_domain_invalid": "域名别名 %s 无效", "alias_empty": "域名地址不能为空", "alias_goto_identical": "别名不能与目标地址相同", - "alias_invalid": "别名地址 %s 非法", - "aliasd_targetd_identical": "域名别名不能与目标域名不能与目标域名相同: %s", + "alias_invalid": "别名地址 %s 无效", + "aliasd_targetd_identical": "域名别名不能与目标域名 %s 相同", "aliases_in_use": "最大别名数必须大于等于 %d", "app_name_empty": "应用名称不能为空", - "app_passwd_id_invalid": "应用密码 ID %s 非法", - "bcc_empty": "BCC目标地址不能为空", - "bcc_exists": "%s类型的BCC映射%s已存在", - "bcc_must_be_email": "BCC目标地址 %s 不是合法的邮箱地址", + "app_passwd_id_invalid": "应用密码 ID %s 无效", + "bcc_empty": "BCC 目标地址不能为空", + "bcc_exists": "%s 类型的 BCC 映射 %s 已存在", + "bcc_must_be_email": "BCC 目标地址 %s 不是有效的邮箱地址", "comment_too_long": "评论太长,最多允许160个字符", "defquota_empty": "每个邮箱的默认配额必须不为0。", - "description_invalid": "%s 的资源描述非法", - "dkim_domain_or_sel_exists": "\"%s\"的DKIM密钥已存在,因此不会覆盖此密钥", - "dkim_domain_or_sel_invalid": "DKIM域名或选择器非法: %s", - "domain_cannot_match_hostname": "域名与主机名不匹配", + "description_invalid": "%s 的资源描述无效", + "dkim_domain_or_sel_exists": "\"%s\"的 DKIM 密钥已存在,因此不会被覆盖", + "dkim_domain_or_sel_invalid": "DKIM 域名或选择器无效: %s", + "domain_cannot_match_hostname": "域名与主机名称不匹配", "domain_exists": "域名 %s 已存在", - "domain_invalid": "域名地址为空或非法", + "domain_invalid": "域名地址为空或无效", "domain_not_empty": "不能删除非空域名 %s", - "domain_not_found": "不能找到域名 %s", + "domain_not_found": "无法找到域名 %s", "domain_quota_m_in_use": "域名配额必须大于等于 %s MiB", - "extra_acl_invalid": "外部发件人地址 \"%s\" 非法", - "extra_acl_invalid_domain": "外部发件人地址 \"%s\" 包含了非法的域名", - "file_open_error": "不能打开文件以写入", + "extra_acl_invalid": "外部发件人地址 \"%s\" 无效", + "extra_acl_invalid_domain": "外部发件人地址 \"%s\" 包含了无效的域名", + "fido2_verification_failed": "FIDO2 验证失败: %s", + "file_open_error": "无法打开文件以写入内容", "filter_type": "过滤器类型错误", "from_invalid": "发件人地址不能为空", - "global_filter_write_error": "不能写入过滤器文件: %s", - "global_map_invalid": "全局规则 ID %s 非法", - "global_map_write_error": "全局规则 ID %s: %s", - "goto_empty": "一个别名地址必须包含至少一个合法的目标地址", - "goto_invalid": "目标地址 %s 不合法", - "ham_learn_error": "学习非垃圾消息错误: %s", - "imagick_exception": "错误: 读取图片时Imagick发生了异常", - "img_invalid": "不能验证图片文件", - "img_tmp_missing": "不能验证图片文件: 找不到临时文件", - "invalid_bcc_map_type": "BCC映射类型非法", - "invalid_destination": "目的地址 \"%s\" 非法", - "invalid_filter_type": "过滤器类型非法", - "invalid_host": "非法主机: %s", - "invalid_mime_type": "mime类型非法", - "invalid_nexthop": "下一跳格式非法", + "global_filter_write_error": "无法写入过滤器文件: %s", + "global_map_invalid": "全局规则 ID %s 无效", + "global_map_write_error": "无法写入全局规则 ID %s: %s", + "goto_empty": "别名地址必须包含至少一个合法的目标地址", + "goto_invalid": "目标地址 %s 无效", + "ham_learn_error": "学习非垃圾邮件错误: %s", + "imagick_exception": "错误: 读取图片时发生了 Imagick 异常", + "img_invalid": "无法验证图片文件", + "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", - "ip_list_empty": "IP允许列表不能为空", + "invalid_recipient_map_new": "新收件人地址无效: %s", + "invalid_recipient_map_old": "原收件人地址无效: %s", + "ip_list_empty": "IP 允许列表不能为空", "is_alias": "%s 已经被作为别名地址使用", "is_alias_or_mailbox": "%s 已经被作为别名地址、邮箱地址或域名别名扩展出的别名地址使用。", "is_spam_alias": "%s 已经被作为临时别名地址使用 (垃圾邮件别名地址)", - "last_key": "最后一个密钥不能被删除,你应该先禁用两步验证。", + "last_key": "无法删除最后一个密钥,你应该先禁用两步验证。", "login_failed": "登录失败", "mailbox_defquota_exceeds_mailbox_maxquota": "默认配额超出配额限制", - "mailbox_invalid": "邮箱名称不合法", + "mailbox_invalid": "邮箱名称无效", "mailbox_quota_exceeded": "配额超出域名配额限制 (最大 %d MiB)", "mailbox_quota_exceeds_domain_quota": "最大配额超出域名配额限制", "mailbox_quota_left_exceeded": "空间不足 (剩余空间: %d MiB)", "mailboxes_in_use": "最大邮箱数必须大于等于 %d", - "malformed_username": "畸形用户名", + "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", + "maxquota_empty": "每个邮箱的最大配额必须不为0", + "mysql_error": "MySQL 错误: %s", + "network_host_invalid": "网络或主机无效: %s", "next_hop_interferes": "%s 与下一跳 %s 冲突", "next_hop_interferes_any": "一个已存在的下一跳与 %s 冲突", - "no_user_defined": "未定义用户", + "nginx_reload_failed": "重启 Nginx 失败: %s", + "no_user_defined": "用户未定义", "object_exists": "对象 %s 已存在", - "object_is_not_numeric": "不是数字值: %s", + "object_is_not_numeric": "%s 不是一个数字", "password_complexity": "密码不符合规则", "password_empty": "密码必须不为空", "password_mismatch": "确认密码不匹配", "policy_list_from_exists": "指定的名称已存在记录", - "policy_list_from_invalid": "记录格式非法", + "policy_list_from_invalid": "记录格式无效", "private_key_error": "私钥错误: %s", - "pushover_credentials_missing": "Pushover token或密钥缺失", - "pushover_key": "Pushover密钥格式错误", - "pushover_token": "Pushover token格式错误", - "quota_not_0_not_numeric": "配额必须为数值且 >= 0", + "pushover_credentials_missing": "Pushover token 或密钥缺失", + "pushover_key": "Pushover 密钥格式错误", + "pushover_token": "Pushover token 格式错误", + "quota_not_0_not_numeric": "配额必须为数字且 >= 0", "recipient_map_entry_exists": "收件人映射条目 \"%s\" 已存在", "redis_error": "Redis 错误: %s", "relayhost_invalid": "中继主机条目 %s 已存在", "release_send_failed": "消息不能被释放: %s", - "reset_f2b_regex": "暂时不能重置正则表达式过滤器,请重试或多等待几秒并重载网页。", - "resource_invalid": "资源名称 %s 非法", - "rl_timeframe": "频率限制时间数不正确", - "rspamd_ui_pw_length": "Rspamd UI密码需要为至少6字符长", + "reset_f2b_regex": "暂时不能重置正则表达式过滤器,请重试或在几秒后重载网页。", + "resource_invalid": "资源名称 %s 无效", + "rl_timeframe": "频率限制时间设置错误", + "rspamd_ui_pw_length": "Rspamd UI 密码至少为为6个字符", "script_empty": "脚本不能为空", - "sender_acl_invalid": "发件人ACL值 %s 非法", - "set_acl_failed": "设置ACL失败", - "settings_map_invalid": "设置规则非法,ID %s", - "sieve_error": "sieve解析器错误: %s", - "spam_learn_error": "垃圾邮件学习错误: %s", + "sender_acl_invalid": "发件人的 ACL 值 %s 无效", + "set_acl_failed": "设置 ACL 失败", + "settings_map_invalid": "设置规则 ID %s 无效", + "sieve_error": "Sieve 解析器错误: %s", + "spam_learn_error": "学习垃圾邮件错误: %s", "subject_empty": "主题必须不为空", - "target_domain_invalid": "目标域名 %s 非法", + "target_domain_invalid": "目标域名 %s 无效", "targetd_not_found": "未找到目标域名 %s", "targetd_relay_domain": "目标域名 %s 是中继域名", "temp_error": "临时错误", "text_empty": "文本必须不为空", - "tls_policy_map_dest_invalid": "策略目标非法", - "tls_policy_map_entry_exists": "TLS策略规则条目 \"%s\" 已存在", - "tls_policy_map_parameter_invalid": "策略参数非法", - "totp_verification_failed": "TOTP认证失败", + "tfa_token_invalid": "TFA token 无效", + "tls_policy_map_dest_invalid": "策略目标无效", + "tls_policy_map_entry_exists": "TLS 策略规则条目 \"%s\" 已存在", + "tls_policy_map_parameter_invalid": "策略参数无效", + "totp_verification_failed": "TOTP 认证失败", "transport_dest_exists": "传输目标 \"%s\" 已存在", - "webauthn_verification_failed": "WebAuthn认证失败: %s", + "webauthn_verification_failed": "WebAuthn 认证失败: %s", "unknown": "发生未知错误", - "unknown_tfa_method": "未知TFA方法", - "unlimited_quota_acl": "ACL设置禁止了无限配额", - "username_invalid": "不能使用用户名 %s", + "unknown_tfa_method": "未知的 TFA 方法", + "unlimited_quota_acl": "ACL 设置禁止了无限配额", + "username_invalid": "用户名 %s 无法使用", "validity_missing": "请设置有效期", "value_missing": "请填入所有值", - "yotp_verification_failed": "Yubico OTP认证失败: %s" + "yotp_verification_failed": "Yubico OTP 认证失败: %s" }, "debug": { "chart_this_server": "图表 (此服务器)", "containers_info": "容器信息", "disk_usage": "磁盘使用", + "docs": "文档", "external_logs": "外部日志", "history_all_servers": "历史 (所有服务器)", "in_memory_logs": "内存日志", - "jvm_memory_solr": "JVM内存使用", - "log_info": "

mailcow 内存日志 收集于Redis列表中并且每分钟自动缩减到 LOG_LINES (%d) 以减少错误(Rowhammer)。\r\n
内存日志不是为了持久化,所有使用内存日志的应用同时也会写入日志到Docker守护程序的默认日志驱动中。\r\n
内存日志应该用于debug容器中的不明显问题。

\r\n

外部日志 通过相应应用提供的API收集。

\r\n

静态日志 大多为不写入日志到Dockerd但仍然需要被持久化的活动日志(API日志外的)。

", - "logs": "日志", - "restart_container": "重启", - "solr_dead": "Solr在启动中、已关闭或已停止运行", - "docs": "文档", + "jvm_memory_solr": "JVM 内存使用", "last_modified": "最后修改", + "log_info": "

Mailcow 的内存日志储存于 Redis 列表中,并且每分钟自动降低到 LOG_LINES (%d) 以减少错误。\r\n
内存日志不是为了持久化储存的,所有使用内存日志的应用同时也会写入日志到 Docker 的守护进程的默认日志驱动中。\r\n
内存日志应该用于分析 (Debug) 容器中不明显的问题。

\r\n

外部日志通过相应应用提供的 API 收集。

\r\n

静态日志大多数为不写入日志到 Docker ,但仍然需要被持久化的活动日志 (API 日志外的)。

", + "login_time": "时间", + "logs": "日志", + "online_users": "在线用户", + "restart_container": "重启", + "service": "服务", "size": "大小", + "solr_dead": "Solr 在启动中、已关闭或已停止", + "solr_status": "Solr 状态", "started_at": "开始于", - "solr_status": "Solr状态", - "uptime": "运行时间", "started_on": "启动于", "static_logs": "静态日志", - "system_containers": "系统和容器" + "success": "成功", + "system_containers": "系统和容器", + "uptime": "运行时间", + "username": "用户名" }, "diagnostics": { - "cname_from_a": "虽然此值记录为 A/AAAA 类型,但只要此记录指向了正确的资源则该行为是被支持的", - "dns_records": "DNS记录", - "dns_records_24hours": "请注意DNS记录的更改可能需要24小时才可以使此页面的当前状态显示正确。此页为你提供了一个可以简单查看如何配置DNS记录和检查你的DNS记录是否正确的方式。", + "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地址或网络", + "allow_from_smtp": "只允许这些 IP 使用 SMTP", + "allow_from_smtp_info": "留空以允许所有发送者。
IPv4/IPv6地址或网络", "allowed_protocols": "允许的协议", "app_name": "应用名称", "app_passwd": "应用密码", - "automap": "尝试自动映射文件夹 (如:\"已发送\", \"Sent\" => \"Sent\")", + "app_passwd_protocols": "应用密码允许的协议", + "automap": "将尝试自动映射文件夹 (例如将 \"已发送消息\" 和 \"已发送\" 均映射到 \"已发送\" 文件夹)", "backup_mx_options": "中继选项", - "bcc_dest_format": "BCC目标地址必须为合法的邮件地址", - "client_id": "客户端ID", - "client_secret": "客户端secret", - "comment_info": "私密评论对用户不可见,公开评论会给用户展示为鼠标悬停显示的提示", - "delete1": "完成后将源邮件删除", - "delete2": "删除目的邮箱中存在但源邮箱中不存在的邮件", - "delete2duplicates": "删除目的邮箱中的重复邮件", + "bcc_dest_format": "BCC 的目标必须是一个有效的电子邮箱地址。
如果你需要向多个地址发送副本,请创建一个别名并在此使用。", + "client_id": "客户端 ID", + "client_secret": "客户端 secret", + "comment_info": "私密评论对用户不可见,公开评论将会在鼠标悬停时作为用户提示显示", + "delete1": "在完成后删除源邮件", + "delete2": "删除在目标邮箱中存在但在源邮箱中不存在的邮件", + "delete2duplicates": "删除目标邮箱中的重复邮件", "delete_ays": "请确认删除。", "description": "描述", - "disable_login": "不允许登录 (仍然会接收邮件)", + "disable_login": "不允许登录 (但仍然会接收邮件)", "domain": "编辑域名", "domain_admin": "编辑域名管理员", "domain_quota": "域名配额", @@ -493,70 +534,80 @@ "dont_check_sender_acl": "为域名 %s (+ 域名别名) 关闭发件人检查", "edit_alias_domain": "编辑域名别名", "encryption": "加密", - "exclude": "排除对象 (正则表达式)", + "exclude": "排除对象 (Regex)", "extended_sender_acl": "外部发件人地址", - "extended_sender_acl_info": "如果可以的话请导入DKIM域名密钥。
\r\n 别忘记将此服务器添加到相应的SPF TXT中。
\r\n 当域名或域名别名被添加时,若其与此外部发件人地址交叠,则外部发件人地址会被移除。
\r\n 填入 @domain.tld 以允许作为 *@domain.tld 发送邮件。", + "extended_sender_acl_info": "当可用时请导入域名的 DKIM 密钥。
\r\n 请注意将此服务器添加到相应的 SPF TXT 记录中。
\r\n 当域名或域名别名被添加时,若其与此外部发件人地址交叠,则外部发件人地址将会被移除。
\r\n 填入 @domain.tld 以允许作为 *@domain.tld 发送邮件。", "force_pw_update": "在下一次登录时强制要求更新密码", "force_pw_update_info": "此用户只能登录到 %s。", "full_name": "全名", "gal": "全球地址簿", - "gal_info": "全球地址簿包含了域名下的所有对象,并且此行为不能被用户更改。如果关闭,用户的 空闲/繁忙 信息将不能在SOGo中显示。重启SOGo以应用更改。", + "gal_info": "全球地址簿包含了域名下的所有对象,并且此行为不能被用户更改。如果关闭,用户的 \"空闲/繁忙\" 的信息将不能在 SOGo 中显示。 重启 SOGo 服务以应用更改。", "generate": "生成", "grant_types": "授权类型", "hostname": "主机名", "inactive": "禁用", "kind": "类型", + "lookup_mx": "应当为一个正则表达式,用于匹配 MX 记录 (例如 .*google\\.com 将转发所有拥有以 google.com 结尾的 MX 记录的邮件)", "mailbox": "编辑邮箱", - "mailbox_quota_def": "默认邮箱配额", - "max_aliases": "最大允许地址别名数", - "max_mailboxes": "最大允许邮箱数", + "mailbox_quota_def": "邮箱默认配额", + "mailbox_relayhost_info": "只适用于邮箱和邮箱别名,不会覆盖域名的中继主机。", + "max_aliases": "最大允许的地址别名数", + "max_mailboxes": "最大允许的邮箱数", "max_quota": "每个邮箱的最大配额 (MiB)", - "maxage": "从远程拉取消息的最大消息年龄限制
(0表示忽略)", - "maxbytespersecond": "最大速率 (Bytes/s)
(0表示不限)", - "mbox_rl_info": "此频率限制应用于SASL登录名,它会匹配邮件的\"来自\"地址。邮箱的频率限制设置会覆盖域名的频率限制值设置。", + "maxage": "从远程拉取消息的最大时间间隔限制
(0表示忽略)", + "maxbytespersecond": "最大速率 (Bytes/s)
(0表示无限制)", + "mbox_rl_info": "此频率限制应用于 SASL 登录名,它会匹配邮件的 \"from\" 地址。邮箱的频率限制设置会覆盖域名的频率限制值设置。", "mins_interval": "轮询间隔 (分钟)", "multiple_bookings": "登记限制", + "none_inherit": "无/继承", "nexthop": "下一跳", "password": "密码", - "password_repeat": "确认密码 (重复输入)", + "password_repeat": "确认密码 (重复)", "previous": "上一页", "private_comment": "私密评论", "public_comment": "公开评论", - "pushover_evaluate_x_prio": "加速高优先级邮件 [X-Priority: 1]", + "pushover": "Pushover", + "pushover_evaluate_x_prio": "优先响应高优先级邮件 [X-Priority: 1]", "pushover_info": "推送通知设置会应用到所有递送到 %s (包括其别名) 的非垃圾邮件。", "pushover_only_x_prio": "只为高优先级邮件开启 [X-Priority: 1]", - "pushover_sender_array": "只为以下发件人邮箱地址开启 (英文逗号分隔)", + "pushover_sender_array": "只为以下发件人邮箱地址开启 (存在多个地址时使用英文逗号分隔)", "pushover_sender_regex": "也可以使用正则表达式过滤发件人", "pushover_text": "通知文本", "pushover_title": "通知标题", - "pushover_vars": "如果没有定义发件人过滤器则会为所有邮件开启通知推送。
正则表达式过滤器可以和普通过滤器分别被定义,并且会依序应用,而不是覆盖另一个。
在文本和标题中可用的变量 (请注意数据保护条例)", + "pushover_vars": "如果没有定义发件人过滤器则会为所有的邮件开启通知推送。
正则表达式过滤器可以和普通过滤器一同使用,并且会按顺序被使用,而不是被覆盖。
在文本和标题中可用的变量 (请注意数据保护)", "pushover_verify": "校验凭证", "quota_mb": "配额 (MiB)", + "quota_warning_bcc": "BCC 配额警告", + "quota_warning_bcc_info": "警告将作为单独的副本发送至以下的收件人。主题将以放置在括号内的用户名做为后缀结尾,例如,Quota warning (user@example.com)。", + "ratelimit": "频率限制", "redirect_uri": "重定向/回调 URL", "relay_all": "中继所有收件人", - "relay_all_info": "↪ 如果选择中继所有,你将需要为每个应该中继的邮件添加一个 (\"盲\") 邮箱。", + "relay_all_info": "↪ 如果选择中继所有收件人,你将需要为每个应该中继的邮件添加一个(\"虚拟\")邮箱。", "relay_domain": "中继这个域名", - "relay_transport_info": "
你可以为此域名定义传输规则以自定义发件目标主机,否则遵照MX记录发送邮件。", - "relay_unknown_only": "只为不存在的邮箱地址中继。已存在的邮箱地址则在本地递送。", + "relay_transport_info": "
注意
你可以为此域名自定义传输规则来指定发件目标主机,否则将遵照 MX 记录发送邮件。", + "relay_unknown_only": "只为不存在的邮箱地址中继。已存在的邮箱地址将在本地发送。", "relayhost": "中继传输", "remove": "删除", - "resource": "日历资源", + "resource": "资源", "save": "保存更改", "scope": "范围", "sender_acl": "允许发送为", "sender_acl_disabled": "发件人检查已关闭", - "sender_acl_info": "如果允许邮箱用户A作为邮箱用户B发送邮件,发件人的地址不会在SOGo中\"来自\"区域自动地作为下拉可选项显示。
\r\n 邮箱用户B需要添加授权以允许邮箱用户A选择B的地址作为发件人;授权方法为,在SOGo中点击左上方邮箱地址右边的菜单按钮(三个点)并授权。", + "sender_acl_info": "如果允许邮箱用户 A 作为邮箱用户 B 发送邮件,发件人的地址不会在 SOGo 中的 \"from\" 区域中作为下拉项显示。
\r\n 邮箱用户 B 需要添加授权以允许邮箱用户 A 选择 B 的地址作为发件人;授权方法为,在 SOGo 中点击左上方邮箱地址右边的菜单按钮 (三个点) 并授权。", "sieve_desc": "简短描述", "sieve_type": "过滤器类型", - "skipcrossduplicates": "跳过其他文件夹中已存在的邮件(保留先存在的邮件)", - "sogo_visible": "SOGo别名显示", - "sogo_visible_info": "此设置只影响SOGo上的可显示对象(指向本地邮箱的共享或非共享别名地址)。如果设为隐藏,别名地址不会作为下拉可选发件人项显示。", + "skipcrossduplicates": "跳过其他文件夹中已存在的邮件(保留已经存在的邮件)", + "sogo_access": "允许直接登录 SOGo", + "sogo_access_info": "在邮箱的用户界面内的单点登录仍然有效。这一设置既不影响对所有其他服务的访问,也不删除或改变用户现有的 SOGo 的配置文件。", + "sogo_visible": "SOGo 显示的别名", + "sogo_visible_info": "此设置只影响 SOGo 上可显示的对象 (指向本地邮箱的共享或非共享别名地址)。如果设置为隐藏,则别名地址不会作为可选发件人的下拉项显示。", "spam_alias": "添加或更改临时别名地址", + "spam_filter": "垃圾邮件过滤器", "spam_policy": "将项目添加到白/黑名单或从其中移除", "spam_score": "自定义垃圾邮件分数", - "subfolder2": "同步到目标邮箱子文件夹
(留空表示不使用子文件夹)", + "subfolder2": "同步到目标邮箱的子文件夹
(留空表示不使用子文件夹)", "syncjob": "编辑同步任务", - "target_address": "目标地址 (英文逗号分隔多个地址)", + "target_address": "目标地址 (存在多个地址时使用英文逗号分隔)", "target_domain": "目标域名", "timeout1": "远程主机连接超时时间", "timeout2": "本地主机连接超时时间", @@ -565,18 +616,35 @@ "username": "用户名", "validate_save": "验证并保存" }, + "fido2": { + "confirm": "确认", + "fido2_auth": "使用FIDO2登录", + "fido2_success": "已注册的设备", + "fido2_validation_failed": "验证失败", + "fn": "别名", + "known_ids": "已注册的 ID", + "none": "禁用", + "register_status": "注册状态", + "rename": "重命名", + "set_fido2": "重新注册 FIDO2 设备", + "set_fido2_touchid": "在 Apple M1 上使用 Touch ID 注册", + "set_fn": "设置别名", + "start_fido2_validation": "开始 FIDO2 验证" + }, "footer": { "cancel": "取消", "confirm_delete": "确认删除", "delete_now": "立即删除", - "delete_these_items": "请确认对以下对象id的更改", + "delete_these_items": "请确认对以下对象 ID 的更改", + "hibp_check": "使用 haveibeenpwned.com 网站检查密码", "hibp_nok": "匹配到密码!存在潜在的使用危险!", - "hibp_ok": "未匹配到密码。", + "hibp_ok": "未匹配到密码", "loading": "请等待...", + "nothing_selected": "未选择", "restart_container": "重启容器", - "restart_container_info": "重要: 可能需要一些时间以完整地重启容器,请等待重启完成。", + "restart_container_info": "重要: 完整的重启容器可能需要花费一些时间,请耐心等待重启完成。", "restart_now": "立即重启", - "restarting_container": "容器重启中,这可能需要一些时间" + "restarting_container": "容器重启中,这可能需要花费一些时间" }, "header": { "administration": "配置和管理", @@ -585,19 +653,21 @@ "mailboxes": "邮箱设置", "mailcow_settings": "配置", "quarantine": "隔离", - "restart_netfilter": "重启netfilter", - "restart_sogo": "重启SOGo", + "restart_netfilter": "重启 netfilter", + "restart_sogo": "重启 SOGo", "user_settings": "用户设置" }, "info": { - "awaiting_tfa_confirmation": "等待TFA确认", + "awaiting_tfa_confirmation": "等待 TFA 确认", "no_action": "没有可适用的操作", - "session_expires": "你的会话会在15秒后过期" + "session_expires": "你的会话将会在15秒之后过期" }, "login": { - "delayed": "请在%s秒后重新登录。", + "delayed": "请在 %s 秒后重新登录。", + "fido2_webauthn": "使用 FIDO2/WebAuthn 登录", "login": "登录", - "mobileconfig_info": "请用邮箱用户登录以下载Apple连接描述文件。", + "mobileconfig_info": "请使用邮箱用户登录以下载 Apple 连接描述文件。", + "other_logins": "Key 登录", "password": "密码", "username": "用户名" }, @@ -607,7 +677,8 @@ "active": "启用", "add": "添加", "add_alias": "添加别名", - "add_bcc_entry": "添加BCC映射", + "add_alias_expand": "在域名别名上拓展别名", + "add_bcc_entry": "添加 BCC 映射", "add_domain": "添加域名", "add_domain_alias": "添加域名别名", "add_domain_record_first": "请先添加一个域名", @@ -615,35 +686,37 @@ "add_mailbox": "添加邮箱", "add_recipient_map_entry": "添加收件人映射", "add_resource": "添加资源", - "add_tls_policy_map": "添加TLS策略规则", + "add_tls_policy_map": "添加 TLS 策略规则", "address_rewriting": "地址重写", "alias": "别名", - "alias_domain_alias_hint": "邮箱别名不会自动应用到域名别名。邮箱别名地址 my-alias@domain 不会 应用到 my-alias@alias-domain (假设 \"alias-domain\" 是 \"domain\" 的域名别名)。
若需要将邮件转发到外部邮箱,请使用sieve过滤器 (查看标签页 \"过滤器\" 或者使用 SOGo -> 转发器) 。", - "alias_domain_backupmx": "域名别名在中继域名下不启用", + "alias_domain_alias_hint": "邮箱别名不会自动应用到域名别名。邮箱别名地址 my-alias@domain 不会 应用到 my-alias@alias-domain (假设 \"alias-domain\" 是 \"domain\" 的域名别名)。
若需要将邮件转发到外部邮箱,请使用 sieve 过滤器 (查看标签页 \"过滤器\" 或者使用 SOGo -> 转发器) 。", + "alias_domain_backupmx": "域名别名在中继域名下不会启用", "aliases": "别名", - "allow_from_smtp": "只允许这些IP使用SMTP", - "allow_from_smtp_info": "留空以允许所有发送者
IPv4/IPv6地址或网络", - "allowed_protocols": "允许的协议", + "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映射用于静默地将邮件转发到另一个邮箱地址。当目标地址为本地地址时则会添加一个收件人映射条目,同发件人映射。
\r\n 递送失败时不会通知本地目标地址。", + "bcc_destination": "BCC 目标地址", + "bcc_destinations": "BCC 目标地址", + "bcc_info": "BCC 映射用于静默地将邮件转发到另一个邮箱地址。当目标地址为本地地址时则会添加一个收件人映射条目,与发件人映射相同。
\r\n 递送失败时不会通知本地目标地址。", "bcc_local_dest": "本地目标地址", - "bcc_map": "BCC映射", - "bcc_map_type": "BCC类型", - "bcc_maps": "BCC映射", + "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类型", + "bcc_type": "BCC 类型", "booking_null": "永远显示为空闲", - "booking_0_short": "永远空闲", + "booking_0_short": "空闲限制", "booking_custom": "严格限制登记数", "booking_custom_short": "严格限制", - "booking_ltnull": "不限制登记数,但会在被登记后显示为繁忙", + "booking_ltnull": "不会限制登记数,但会在被登记后显示为繁忙", "booking_lt0_short": "宽松限制", + "catch_all": "接收所有", "daily": "每天", "deactivate": "禁用", "description": "描述", @@ -660,21 +733,24 @@ "excludes": "除了", "filter_table": "筛选表格", "filters": "过滤器", - "fname": "全名", + "fname": "全称", + "goto_ham": "学习为非垃圾邮件", + "goto_spam": "学习为垃圾邮件", "hourly": "每小时", "in_use": "使用数 (%)", "inactive": "禁用", "insert_preset": "插入示例预设 \"%s\"", "kind": "类型", - "last_mail_login": "最后的邮箱登录", - "last_run": "最后运行", + "last_mail_login": "最后一次邮箱登录", + "last_pw_change": "最后一次密码修改", + "last_run": "最后一次运行", "last_run_reset": "下一次运行", "mailbox": "邮箱", + "mailbox_defaults": "默认设置", + "mailbox_defaults_info": "调整新邮箱的默认设置", "mailbox_defquota": "默认邮箱大小", "mailbox_quota": "最大邮箱大小", "mailboxes": "邮箱", - "mailbox_defaults": "默认设置", - "mailbox_defaults_info": "配置新邮箱的默认设置", "mins_interval": "间隔 (分钟)", "msg_num": "消息 #", "multiple_bookings": "登记限制", @@ -682,69 +758,85 @@ "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_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": "收件人映射", "remove": "删除", - "resources": "日历资源", + "resources": "资源", "running": "运行中", - "set_postfilter": "标记为postfilter", - "set_prefilter": "标记为prefilter", - "sieve_info": "你可以为每个用户存储多个过滤器,但只能同时启用一个prefilter和一个postfilter。
\r\n过滤器将按列表中的顺序依次执行,下一个脚本不会因为上一个脚本失败或\"keep;\"而停止运行。更改全局sieve脚本会重启Dovecot。

全局sieve prefilter → prefilter → 用户脚本 → postfilter → 全局sieve postfilter", - "sieve_preset_1": "丢弃含有潜在危险文件格式的邮箱", + "sender": "发件人", + "set_postfilter": "标记为 postfilter", + "set_prefilter": "标记为 prefilter", + "sieve_info": "你可以为每个用户存储多个过滤器,但只能同时启用一个 prefilter 和一个 postfilter 。
\r\n过滤器将按列表中的顺序依次执行,下一个脚本不会因为上一个脚本运行失败或保留运行而停止运行。更改全局 sieve 脚本会重启 Dovecot。

全局 sieve prefilter • prefilter • 用户脚本 • postfilter • 全局 sieve postfilter", + "sieve_preset_1": "丢弃包含有潜在危险文件格式的邮箱", "sieve_preset_2": "标记来至指定发件人的邮件为已读", - "sieve_preset_3": "静默删除,并停止继续运行sieve脚本", - "sieve_preset_4": "移动到收件箱,并停止继续运行sieve脚本", + "sieve_preset_3": "静默删除,并停止继续运行 sieve 脚本", + "sieve_preset_4": "移动到收件箱,并停止继续运行 sieve 脚本", "sieve_preset_5": "自动回复 (休假)", - "sieve_preset_6": "拒绝邮件并反馈", - "sieve_preset_7": "重定向邮件并保留/删除", + "sieve_preset_6": "拒绝接收邮件并通知", + "sieve_preset_7": "重定向邮件并保留或删除", "sieve_preset_8": "删除发件人发送给自己别名地址的邮件", - "sieve_preset_header": "请看下方的示例预设。 查看 Wikipedia 以了解更多细节。", - "sogo_visible": "SOGo别名显示", - "sogo_visible_n": "在SOGo中隐藏别名", - "sogo_visible_y": "在SOGo中显示别名", + "sieve_preset_header": "请看下方的示例预设。 查看 Sieve 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": "目标域名", - "tls_enforce_in": "强制入站TLS", - "tls_enforce_out": "强制出站TLS", + "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_parameters_info": "留空或填入相关参数,例如: protocols=!SSLv2 ciphers=medium exclude=3DES", "tls_map_policy": "策略", - "tls_policy_maps": "TLS策略规则", - "tls_policy_maps_info": "此策略规则覆盖用户的出站TLS策略设置。
\r\n 查看 \"smtp_tls_policy_maps\"文档 以了解更多细节。", - "tls_policy_maps_enforced_tls": "这些策略同时会影响邮箱用户的出站TLS连接行为。如果在下方没有任何策略,则用户会应用 smtp_tls_mandatory_protocolssmtp_tls_mandatory_ciphers 指定的默认值", - "tls_policy_maps_long": "出站TLS策略规则重写", - "toggle_all": "选择/取消所有", + "tls_policy_maps": "TLS 策略规则", + "tls_policy_maps_enforced_tls": "这些策略会同时影响邮箱用户的出站 TLS 连接行为。如果在下方没有任何策略,则用户会使用 smtp_tls_mandatory_protocolssmtp_tls_mandatory_ciphers 指定的默认值。", + "tls_policy_maps_info": "此策略规则会覆盖用户的出站 TLS 策略设置。
\r\n 查看 \"smtp_tls_policy_maps\" 文档以了解更多细节。", + "tls_policy_maps_long": "重写出站 TLS 策略规则", + "toggle_all": "选择所有/取消所有", "username": "用户名", "waiting": "等待中", "weekly": "每周", "yes": "✓" }, "oauth2": { - "access_denied": "请作为邮箱所有者登录以使用OAuth2授权", + "access_denied": "请作为邮箱所有者登录以使用 OAuth2 授权", "authorize_app": "授权应用", "deny": "拒绝", "permit": "授权应用", "profile": "个人资料", - "profile_desc": "查看个人信息: 用户名,全面,创建时间,修改时间,启用状态", - "scope_ask_permission": "一个应用请求了以下权限" + "profile_desc": "查看个人信息: 用户名,全称,创建时间,修改时间,状态", + "scope_ask_permission": "应用请求了以下权限" }, "quarantine": { "action": "操作", @@ -752,61 +844,70 @@ "check_hash": "搜索文件特征 @ VT", "confirm": "确认", "confirm_delete": "确认删除此元素。", - "danger": "危险性", - "deliver_inbox": "递送到收件箱", - "disabled_by_config": "当前系统设置关闭了隔离功能,请设置 \"每个邮箱保留隔离项目数\" 和 \"最大文件大小\" 以开启隔离。", + "danger": "危险等级", + "deliver_inbox": "发送到收件箱", + "disabled_by_config": "当前系统设置关闭了隔离功能,请通过设置 \"每个邮箱保留的隔离项目数\" 以及 \"最大文件大小\" 以开启隔离。", "download_eml": "下载 (.eml)", "empty": "结果为空", - "high_danger": "高危险", + "high_danger": "高危险等级", "info": "信息", "junk_folder": "垃圾箱", - "learn_spam_delete": "学习为垃圾并删除", - "low_danger": "低危险", - "medium_danger": "中危险", - "neutral_danger": "中性", - "notified": "已通知", - "qhandler_success": "成功向系统发送请求,现在你可以关闭窗口了。", + "learn_spam_delete": "学习为垃圾邮件并删除", + "low_danger": "低危险等级", + "medium_danger": "中危险等级", + "neutral_danger": "无危险等级", + "notified": "已发送通知", + "qhandler_success": "已成功向系统发送请求,现在你可以关闭这个窗口了。", "qid": "Rspamd QID", - "qinfo": "隔离系统会将被拒绝的邮件以及作为拷贝发送到垃圾箱的邮件保存到数据库中 (发件人会知道)。\r\n
\"学习为垃圾并删除\" 会根据贝叶斯定理将消息作为垃圾学习并计算其模糊特征以拒绝未来收到相似消息。\r\n
请注意,取决于你的系统资源,学习多个消息可能会花费较长时间。
黑名单中项目会被隔离系统排除。", + "qinfo": "隔离系统会把已被拒绝接收的邮件以及作为拷贝发送到垃圾箱的邮件保存到数据库中 (发件人会知道)。\r\n
\"学习为垃圾并删除\" 会根据贝叶斯定理将消息作为垃圾学习并计算其模糊特征以拒绝未来收到相似消息。\r\n
请注意,这取决于你的系统资源,学习多个消息可能会花费较长时间。
黑名单中项目会被隔离系统排除。", "qitem": "隔离项目", "quarantine": "隔离", "quick_actions": "操作", - "rcpt": "收件人", - "received": "已接收", - "rejected": "已拒绝", - "recipients": "收件人", - "refresh": "刷新", - "release": "释放", - "release_body": "我们已在此消息中将你的消息附为eml文件", - "release_subject": "存在潜在危险的隔离文件 %s", - "remove": "删除", - "rspamd_result": "Rspamd结果", - "sender": "发件人 (SMTP)", - "sender_header": "发件人 (\"From\" 头)", - "type": "类型", - "quick_release_link": "打开快速释放链接", "quick_delete_link": "打开快速删除链接", "quick_info_link": "打开详情链接", + "quick_release_link": "打开快速移除链接", + "rcpt": "收件人", + "received": "已接收", + "recipients": "收件人", + "refresh": "刷新", + "rejected": "已拒绝", + "release": "移除", + "release_body": "我们已在此消息中将你的消息作为 eml 附件文件", + "release_subject": "存在潜在危险的隔离文件 %s", + "remove": "删除", + "rewrite_subject": "重写主题", + "rspamd_result": "Rspamd 结果", + "sender": "发件人 (SMTP)", + "sender_header": "发件人 (\"From\" 头)", + "settings_info": "被隔离的元素的最大数目: %s
电子邮件的最大大小: %s MiB", "show_item": "显示项目", - "spam": "垃圾", + "spam": "垃邮件圾", "spam_score": "分数", "subj": "主题", "table_size": "表格尺寸", "table_size_show_n": "显示 %s 个项目", - "text_from_html_content": "内容 (已转换 html)", + "text_from_html_content": "内容 (已转换为 html)", "text_plain_content": "内容 (text/plain)", - "toggle_all": "选择/取消所有" + "toggle_all": "选择所有/取消所有", + "type": "类型" + }, + "ratelimit": { + "disabled": "禁用", + "second": "msgs / 秒", + "minute": "msgs / 分钟", + "hour": "msgs / 小说", + "day": "msgs / 天" }, "start": { "help": "显示/隐藏 帮助面板", - "imap_smtp_server_auth_info": "请使用你的完整邮箱地址并使用PLAIN(明文)认证方法。
\r\n你的登录数据会被服务端强制加密。", - "mailcow_apps_detail": "使用mailcow应用访问你的邮件、日历、联系人和更多。", - "mailcow_panel_detail": "域名管理员 可以创建、修改或删除邮箱胡别名,更改分配给其的域名并读取更多域名相关信息。
\r\n邮箱用户 可以创建临时别名 (垃圾邮件别名),更改他们的密码和垃圾过滤器设置。" + "imap_smtp_server_auth_info": "请使用你的完整邮箱地址并使用 PLAIN (明文) 认证方法。
\r\n你的登录数据会被服务端强制加密。", + "mailcow_apps_detail": "使用 Mailcow 应用访问你的邮件、日历、联系人以及更多内容。", + "mailcow_panel_detail": "域名管理员可以创建、修改或删除邮箱的别名,更改分配给用户的域名并读取更多域名相关信息。
\r\n邮箱用户可以创建临时别名 (垃圾邮件别名),更改密码以及设置垃圾邮件过滤器。" }, "success": { - "acl_saved": "已保存对象 %s 的ACL", + "acl_saved": "已保存对象 %s 的 ACL 设置", "admin_added": "已添加管理员 %s", - "admin_api_modified": "已保存API更改", + "admin_api_modified": "已保存 API 的更改", "admin_modified": "已保存管理员更改", "admin_removed": "已删除管理员 %s", "alias_added": "已添加别名地址 %s (%d)", @@ -817,95 +918,102 @@ "aliasd_modified": "已保存域名别名 %s 更改", "app_links": "已保存应用链接更改", "app_passwd_added": "已添加新的应用密码", - "app_passwd_removed": "已删除应用密码,ID %s", - "bcc_deleted": "已删除BCC映射条目: %s", - "bcc_edited": "已编辑BCC映射条目 %s", - "bcc_saved": "已保存BCC映射条目", + "app_passwd_removed": "已删除应用密码 ID %s", + "bcc_deleted": "已删除 BCC 映射条目: %s", + "bcc_edited": "已编辑 BCC 映射条目 %s", + "bcc_saved": "已保存 BCC 映射条目", "db_init_complete": "数据库初始化完成", - "delete_filter": "已删除过滤器,ID %s", + "delete_filter": "已删除过滤器 ID %s", "delete_filters": "已删除过滤器: %s", - "deleted_syncjob": "已删除同步任务,ID %s", + "deleted_syncjob": "已删除同步任务 ID %s", "deleted_syncjobs": "已删除同步任务: %s", - "dkim_added": "已保存DKIM密钥 %s", - "dkim_duplicated": "已复制域名 %s 的DKIM密钥到 %s", - "dkim_removed": "已删除DKIM密钥 %s", + "dkim_added": "已保存 DKIM 密钥 %s", + "domain_add_dkim_available": "DKIM 密钥已经存在", + "dkim_duplicated": "已复制域名 %s 的 DKIM 密钥到 %s", + "dkim_removed": "已删除 DKIM 密钥 %s", "domain_added": "已添加域名 %s", "domain_admin_added": "已添加域名管理员 %s", "domain_admin_modified": "已保存域名管理员 %s 更改", "domain_admin_removed": "已删除域名管理员 %s", - "domain_modified": "已保存域名 %s 更改", + "domain_modified": "已保存域名 %s 的更改", "domain_removed": "已删除域名 %s", - "dovecot_restart_success": "Dovecot重启成功", - "eas_reset": "已重置用户 %s 的ActiveSync设备", - "f2b_modified": "已保存Fail2ban参数更改", + "dovecot_restart_success": "Dovecot 重新启动成功", + "eas_reset": "已重置用户 %s 的 ActiveSync 设备", + "f2b_modified": "已保存 Fail2ban 参数的更改", "forwarding_host_added": "已添加转发主机 %s", "forwarding_host_removed": "已删除转发主机 %s", "global_filter_written": "成功将过滤器写入到文件", "hash_deleted": "已删除特征", "item_deleted": "成功删除项目 %s", - "item_released": "已释放项目 %s", + "item_released": "已移除项目 %s", "items_deleted": "成功删除项目 %s", - "items_released": "已释放选中的项目", - "learned_ham": "成功学习ID %s为非垃圾", + "items_released": "已移除选中的项目", + "learned_ham": "成功学习 ID %s 为非垃圾邮件", "license_modified": "已保存许可证更改", "logged_in_as": "登录为 %s", "mailbox_added": "已添加邮箱 %s", - "mailbox_modified": "已保存邮箱 %s 更改", + "mailbox_modified": "已保存邮箱 %s 的更改", "mailbox_removed": "已删除邮箱 %s", + "nginx_reloaded": "Nginx 已重新启动", "object_modified": "已保存对象 %s 更改", - "pushover_settings_edited": "成功设置Pushover设置,请校验凭证", - "qlearn_spam": "消息 ID %s 已被学习为垃圾并被删除", + "password_policy_saved": "已成功保存密码规则", + "pushover_settings_edited": "已成功设置 Pushover,请重新校验凭证", + "qlearn_spam": "消息 ID %s 已被学习为垃圾邮件并被删除", "queue_command_success": "成功执行配额命令", - "recipient_map_entry_deleted": "已删除接收人映射,ID %s", + "recipient_map_entry_deleted": "已删除接收人映射 ID %s", "recipient_map_entry_saved": "已保存接收人映射条目 \"%s\"", "relayhost_added": "已添加中继主机 %s", "relayhost_removed": "已删除中继主机 %s", - "reset_main_logo": "重置为默认logo", + "reset_main_logo": "重置为默认 Logo", "resource_added": "已添加资源 %s", - "resource_modified": "已保存资源 %s 更改", + "resource_modified": "已保存资源 %s 的更改", "resource_removed": "已删除资源 %s", "rl_saved": "已保存 %s", - "rspamd_ui_pw_set": "成功设置Rspamd UI密码", + "rspamd_ui_pw_set": "成功设置 Rspamd UI 的密码", "saved_settings": "已保存设置", "settings_map_added": "已添加设置规则", - "settings_map_removed": "已删除设置规则,ID %s", - "sogo_profile_reset": "已重置用户 %s 的SOGo个人资料", - "tls_policy_map_entry_deleted": "已删除TLS策略规则,ID %s", - "tls_policy_map_entry_saved": "已保存TLS策略规则 \"%s\"", - "ui_texts": "已保存UI文本更改", - "upload_success": "成功上传文件", - "verified_totp_login": "TOTP登录验证成功", - "verified_webauthn_login": "WebAuthn登录验证成功", - "verified_yotp_login": "Yubico OTP登录验证成功" + "settings_map_removed": "已删除设置规则 ID %s", + "sogo_profile_reset": "已重置用户 %s 的 SOGo 个人资料", + "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": { - "api_register": "%s 使用 Yubico Cloud API,请 在此 为你的密钥获取API密钥", + "api_register": "%s 使用了 Yubico Cloud API,请在此为你的密钥获取 API 密钥", "confirm": "确认", - "confirm_totp_token": "请输入生成的token以确认更改", + "confirm_totp_token": "请输入生成的验证码以确认更改", "delete_tfa": "关闭两步验证", "disable_tfa": "在下一次成功登录前关闭两步验证", - "enter_qr_code": "如果你的设备不能扫描QR码,输入此TOTP码", - "error_code": "错误码", + "enter_qr_code": "如果你的设备无法扫描 QR 码,请输入此 TOTP 码", + "error_code": "故障代码", "init_webauthn": "初始化中,请等待...", - "key_id": "你的YubiKey的标识", - "key_id_totp": "你的密钥的标识", + "key_id": "你的设备的标识符", + "key_id_totp": "你的密钥的名称", "none": "禁用", - "reload_retry": "- (如果一直出现错误,重启浏览器)", - "scan_qr_code": "请用你认证应用扫描或手动输入此码。", + "reload_retry": "- (如果一直出现错误,请尝试重启浏览器)", + "scan_qr_code": "请用你的认证应用扫描或手动输入 TOTP 码。", "select": "请选择", - "set_tfa": "设置两步验证方法", + "set_tfa": "启用两步验证方法", "start_webauthn_validation": "开始认证", - "tfa": "两步验证(2FA)", - "totp": "TOTP认证 (Google Authenticator、Authy等)", - "webauthn": "WebAuthn认证", - "waiting_usb_auth": "等待USB设备...

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

请在上方输入你的密码并请触碰你的WebAuthn USB设备上的按钮以确认注册WebAuthn设备。", - "yubi_otp": "Yubico OTP认证" + "tfa": "两步验证 (2FA)", + "tfa_token_invalid": "TFA token 无效", + "totp": "TOTP 认证 (Google Authenticator、Authy 等)", + "u2f_deprecated": "似乎你的密钥是使用废弃的 U2F 方法获得的。我们将为停用你的两步验证并删除该密钥。", + "u2f_deprecated_important": "请在管理面板上使用新的 WebAuthn 方法获得你的密钥。", + "webauthn": "WebAuthn 认证", + "waiting_usb_auth": "等待 USB 设备中...

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

请在上方输入你的密码并请触碰你的 WebAuthn USB 设备上的按钮以确认注册该 WebAuthn 设备。", + "yubi_otp": "Yubico OTP 认证" }, "user": { "action": "操作", "active": "启用", - "active_sieve": "启用的过滤器", + "active_sieve": "已启用的过滤器", "advanced_settings": "高级设置", "alias": "别名", "alias_create_random": "生成随机别名", @@ -916,85 +1024,110 @@ "alias_time_left": "剩余时间", "alias_valid_until": "有效至", "aliases_also_send_as": "同时允许发送为", - "aliases_send_as_all": "关闭发件人可访性检查的域名(别名)", - "app_hint": "应用密码是你登录 IMAP 和 SMTP 时的可选替代密码,用户名保持不变。
应用密码不作用于SOGo (包括 ActiveSync) ", + "aliases_send_as_all": "已关闭发件人可访性检查的域名和域名别名", + "app_hint": "应用密码是你登录 IMAP 和 SMTP 时的可选替代密码,用户名仍然保持不变。
应用密码不适用于 SOGo (包括 ActiveSync) ", + "allowed_protocols": "允许使用的协议", "app_name": "应用名称", "app_passwds": "应用密码", - "apple_connection_profile": "Apple连接描述文件", - "apple_connection_profile_complete": "此连接描述文件包括提供给Apple设备的IMAP和SMTP配置参数并包括CalDAV (日历) 和 CardDAV (联系人) 访问路径。", - "apple_connection_profile_mailonly": "此连接描述文件包括提供给Apple设备的IMAP和SMTP配置参数。", + "apple_connection_profile": "Apple 连接描述文件", + "apple_connection_profile_complete": "此连接描述文件包括提供给 Apple 设备的 IMAP 和 SMTP 配置参数,并会包括 CalDAV (日历) 和 CardDAV (联系人) 的访问路径。", + "apple_connection_profile_mailonly": "此连接描述文件包括提供给 Apple 设备的 IMAP 和 SMTP 配置参数。", + "apple_connection_profile_with_app_password": "一个新的应用程序密码将会被生成并添加到该配置文件中,因此在设备设置时不需要输入密码。请不要随意分享该文件,因为它包含你的邮箱的完全访问权限。", "change_password": "更改密码", - "client_configuration": "显示邮箱客户端和智能手机配置指南", + "change_password_hint_app_passwords": "你的账户有 {{number_of_app_passwords}} 个应用密码,这些密码将不会被更改。如果需要管理这些密码,请访问应用密码标签。", + "clear_recent_successful_connections": "清除成功匹配的连接", + "client_configuration": "显示邮箱客户端和智能手机的配置指南", "create_app_passwd": "添加应用密码", "create_syncjob": "添加同步任务", + "created_on": "添加于", "daily": "每日", "day": "日", "delete_ays": "请确认删除。", "direct_aliases": "直接别名", - "direct_aliases_desc": "垃圾邮件过滤和TLS策略会作用于直接别名。", - "eas_reset": "重置ActiveSync设备缓存", - "eas_reset_help": "在许多情况下,重置设备缓存可以帮助恢复错误的ActiveSync资料。
注意: 所有元素会被重新下载!", + "direct_aliases_desc": "垃圾邮件过滤和 TLS 策略会作用于直接别名。", + "direct_protocol_access": "该邮箱用户可以直接外部访问以下的协议和应用程序。该选项由你的管理员进行设置。并可以创建应用密码,以授予对个别协议和应用的访问权限。
\"登录到 Webmail\" 按钮提供到 SOGo 的单点登录方式,并且始终可用。", + "eas_reset": "重置 ActiveSync 设备缓存", + "eas_reset_help": "在许多情况下,重置设备缓存可以帮助恢复错误的 ActiveSync 资料。
注意: 所有元素将会被重新下载!", "eas_reset_now": "立即重置", "edit": "编辑", "email": "邮件", "email_and_dav": "邮件、日历和联系人", + "empty": "结果为空", "encryption": "加密", "excludes": "排除", "expire_in": "过期于", - "force_pw_update": "你必须设置一个新密码以继续使用群件相关服务。", + "fido2_webauthn": "FIDO2/WebAuthn", + "force_pw_update": "你必须设置一个新密码才能继续使用群组的相关服务。", + "from": "从", "generate": "生成", "hour": "小时", "hourly": "每小时", "hours": "小时", "in_use": "已使用", "interval": "间隔", - "is_catch_all": "收取域名下所有邮件", - "last_mail_login": "最后邮箱登录", - "last_run": "最后运行", + "is_catch_all": "接收该域名下的所有邮件", + "last_mail_login": "最后一次邮箱登录", + "last_pw_change": "最后一次密码修改", + "last_run": "最后一次运行", + "last_ui_login": "最后一次 UI 登录信息", "loading": "加载中...", + "login_history": "登录历史", + "mailbox": "Mailbox", "mailbox_details": "邮箱详情", + "mailbox_general": "通用设置", + "mailbox_settings": "邮箱设置", "messages": "消息", + "month": "月", + "months": "月", "never": "从不", "new_password": "新密码", "new_password_repeat": "确认密码 (重复)", - "no_active_filter": "没有启用的过滤器", - "no_last_login": "没有最后UI登录信息", + "no_active_filter": "没有已启用的过滤器", + "no_last_login": "没有最后一次 UI 登录信息", "no_record": "没有记录", + "open_logs": "打开日志", + "open_webmail_sso": "登录到 Webmail", "password": "密码", "password_now": "当前密码 (确认更改)", - "password_repeat": "密码 (重复)", - "pushover_evaluate_x_prio": "加速高优先级邮件 [X-Priority: 1]", + "password_repeat": "确认密码 (重复)", + "pushover_evaluate_x_prio": "优先响应高优先级邮件 [X-Priority: 1]", "pushover_info": "推送通知设置会应用到所有递送到 %s (包括其别名) 的非垃圾邮件。", "pushover_only_x_prio": "只为高优先级邮件开启 [X-Priority: 1]", - "pushover_sender_array": "只为以下发件人邮箱地址开启 (英文逗号分隔)", + "pushover_sender_array": "只为以下发件人邮箱地址开启 (存在多个地址时使用英文逗号分隔)", "pushover_sender_regex": "也可以使用正则表达式过滤发件人", "pushover_text": "通知文本", "pushover_title": "通知标题", - "pushover_vars": "如果没有定义发件人过滤器则会为所有邮件开启通知推送。
正则表达式过滤器可以和普通过滤器分别被定义,并且会依序应用,而不是覆盖另一个。
在文本和标题中可用的变量 (请注意数据保护条例)", + "pushover_vars": "如果没有定义发件人过滤器则会为所有的邮件开启通知推送。
正则表达式过滤器可以和普通过滤器一同使用,并且会按顺序被使用,而不是被覆盖。
在文本和标题中可用的变量 (请注意数据保护)", "pushover_verify": "验证凭证", + "q_add_header": "垃圾邮件文件夹", + "q_all": "全部类别", + "q_reject": "拒绝接收", + "quarantine_category": "隔离通知类别", + "quarantine_category_info": "隔离通知类别\"拒绝接收\"包括所有被拒绝的邮件,而\"垃圾邮件文件夹\"将通知用户被放入垃圾邮件文件夹的邮件。", "quarantine_notification": "隔离通知", - "quarantine_notification_info": "一旦某通知已被发送,其会被标记为\"已通知\"且不会被再次发送。", + "quarantine_notification_info": "一但通知被发送,其会被标记为\"已通知\"且不会被再次发送。", + "recent_successful_connections": "成功匹配的连接", "remove": "删除", "running": "运行中", "save": "保存更改", "save_changes": "保存更改", "sender_acl_disabled": "发件人检查已关闭", "shared_aliases": "共享别名地址", - "shared_aliases_desc": "用户设置如垃圾过滤器和加密策略等不会应用到共享别名地址。共享别名地址只能应用域名级别的垃圾过滤器且只能被管理员修改。", - "show_sieve_filters": "显示启用的用户sieve过滤器", - "sogo_profile_reset": "重置SOGo个人资料", - "sogo_profile_reset_help": "此操作会不可恢复地删除用户SOGo个人资料并删除所有联系人和日历数据。", + "shared_aliases_desc": "用户设置的垃圾邮件过滤器和加密策略等不会应用到共享别名地址。共享别名地址只能应用域名级别的垃圾邮件过滤器,并且只能被管理员修改。", + "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": "垃圾邮件过滤器", + "spamfilter_behavior": "分数", "spamfilter_bl": "黑名单", - "spamfilter_bl_desc": "黑名单中地址总是会被标记为垃圾邮件。被拒绝的邮件不会进入隔离。此处可以使用通配符\"*\"。此过滤器也会应用到直接别名(只指向一个目标邮箱),但不会应用到\"捕获所有\"别名和邮箱地址本身。", + "spamfilter_bl_desc": "黑名单中地址总是会被标记为垃圾邮件。被拒绝的邮件不会进入隔离区。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。", "spamfilter_default_score": "默认值", - "spamfilter_green": "绿色: 此消息不是垃圾", - "spamfilter_hint": "第一个值表示\"低垃圾分数\",第二个值表示\"高垃圾分数\"。", - "spamfilter_red": "红色: 此消息是垃圾并且会被服务器拒绝", + "spamfilter_green": "绿色: 此消息不是垃圾邮件", + "spamfilter_hint": "第一个值表示\"低垃圾邮件分数\",第二个值表示\"高垃圾邮件分数\"。", + "spamfilter_red": "红色: 此消息是垃圾邮件并且会被服务器拒收", "spamfilter_table_action": "操作", "spamfilter_table_add": "添加项目", "spamfilter_table_domain_policy": "n/a (域名策略)", @@ -1002,40 +1135,53 @@ "spamfilter_table_remove": "删除", "spamfilter_table_rule": "规则", "spamfilter_wl": "白名单", - "spamfilter_wl_desc": "白名单中地址永远不会被标记为垃圾邮件。此处可以使用通配符\"*\"。此过滤器也会应用到直接别名(只指向一个目标邮箱),但不会应用到\"捕获所有\"别名和邮箱地址本身。", - "spamfilter_yellow": "黄色: 此为垃圾消息,会被标记为垃圾并且移入垃圾文件夹", + "spamfilter_wl_desc": "白名单中地址永远不会被标记为垃圾邮件。此处可以使用通配符 \"*\"。此过滤器也会应用到直接别名 (只指向一个目标邮箱),但不会应用到\"接收所有\"别名和邮箱地址本身。", + "spamfilter_yellow": "黄色: 此为垃圾邮件,会被标记为垃圾邮件并且移入垃圾邮件文件夹", "status": "状态", "sync_jobs": "同步任务", - "tag_handling": "处理有标签的邮件", - "tag_help_example": "有标签的邮箱地址示例: me+Facebook@example.org", - "tag_help_explain": "置于子文件夹: 在INBOX(收件箱)下创建一个以标签名命名的子文件夹 (\"INBOX/Facebook\")。
\r\n置于主题: 标签名会被前置到邮件主题, 如: \"[Facebook] 我的新闻\"。", + "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": "用户名或密码错误", + "tag_handling": "处理带有标签的邮件", + "tag_help_example": "带有标签的邮箱地址示例: me+Facebook@example.org", + "tag_help_explain": "置于子文件夹: 在 INBOX (收件箱) 下创建一个以标签名命名的子文件夹 (\"INBOX/Facebook\")。
\r\n置于主题: 标签名会被前置到邮件主题, 例如: \"[Facebook] My News\"。", "tag_in_none": "不处理", "tag_in_subfolder": "置于子文件夹", "tag_in_subject": "置于主题", "text": "文本", "title": "标题", - "tls_enforce_in": "强制入站TLS", - "tls_enforce_out": "强制出站TLS", + "tls_enforce_in": "强制入站使用 TLS", + "tls_enforce_out": "强制出站使用 TLS", "tls_policy": "加密策略", - "tls_policy_warning": "警告: 如果你决定强制加密邮箱传输,你有可能丢失邮件。
不支持加密的邮件系统将不能与此服务器交换邮件。
此选项会应用到你的主邮件地址(登录名)、域名别名对应的地址和只指向此一个邮箱(非共享)的别名地址。", + "tls_policy_warning": "警告: 如果你决定强制使用加密邮箱传输,你有可能丢失邮件。
不支持加密的邮件系统将无法与此服务器交换邮件。
此选项会应用到你的主邮箱地址 (登录名)、域名别名对应的地址和只指向此一个邮箱 (非共享) 的别名地址。", "user_settings": "用户设置", "username": "用户名", "verify": "验证", "waiting": "等待中", "week": "周", "weekly": "每周", - "weeks": "周" + "weeks": "周", + "with_app_password": "包含应用密码", + "year": "年", + "years": "年" }, "warning": { - "cannot_delete_self": "不能删除已登录用户", - "domain_added_sogo_failed": "域名已添加但是重启Dovecot失败,请检查日志。", - "dovecot_restart_failed": "Dovecot重启失败,请检查日志", + "cannot_delete_self": "不能删除已登录的用户", + "domain_added_sogo_failed": "域名已添加但是重启 Dovecot 失败,请检查相关日志。", + "dovecot_restart_failed": "Dovecot 重启失败,请检查相关日志", "fuzzy_learn_error": "模糊特征学习失败: %s", "hash_not_found": "找不到特征或已被删除", - "ip_invalid": "跳过的非法IP: %s", - "no_active_admin": "不能禁用最后一个启用的管理员", - "quota_exceeded_scope": "域名配额超出: 此域名下现在只能创建无限容量的邮箱。", - "session_token": "表单字段非法: 字段不匹配", - "session_ua": "表单字段非法: User-Agent校验错误" + "ip_invalid": "跳过的无效 IP: %s", + "is_not_primary_alias": "跳过的非主要别名 %s", + "no_active_admin": "不能禁用最后一个被启用的管理员", + "quota_exceeded_scope": "域名配额超标: 此域名下现在只能创建无限容量的邮箱。", + "session_token": "表单字段无效: Token 不匹配", + "session_ua": "表单字段无效: User-Agent 校验错误" } } From c46a1c1e2f76543968ff259e6dd5eaf80ada89dd Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 14 Nov 2022 22:51:19 +0100 Subject: [PATCH 010/170] [GH-Actions][actionpr] Update to v0.5.3 --- .github/workflows/pr_to_nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_to_nightly.yml b/.github/workflows/pr_to_nightly.yml index fd9e4946..54dbda34 100644 --- a/.github/workflows/pr_to_nightly.yml +++ b/.github/workflows/pr_to_nightly.yml @@ -12,7 +12,7 @@ jobs: with: fetch-depth: 0 - name: Run the Action - uses: devops-infra/action-pull-request@v0.5.1 + uses: devops-infra/action-pull-request@v0.5.3 with: github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }} title: Automatic PR to nightly from ${{ github.event.repository.updated_at}} From 6875baf64ca249c4fd08ca6b9faff92184cfac76 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 15 Nov 2022 19:43:03 +0100 Subject: [PATCH 011/170] Update issue template --- .github/ISSUE_TEMPLATE/Bug_report.yml | 166 ++++++++++++++++++-------- 1 file changed, 114 insertions(+), 52 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Bug_report.yml b/.github/ISSUE_TEMPLATE/Bug_report.yml index 6134a9ad..2fe7082c 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/Bug_report.yml @@ -7,8 +7,8 @@ body: label: Contribution guidelines description: Please read the contribution guidelines before proceeding. options: - - label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree - required: true + - label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree + required: true - type: checkboxes attributes: label: I've found a bug and checked that ... @@ -26,70 +26,132 @@ body: attributes: label: Description description: Please provide a brief description of the bug in 1-2 sentences. If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI. + render: text validations: required: true - type: textarea attributes: - label: Logs - description: Please take a look at the [official documentation](https://mailcow.github.io/mailcow-dockerized-docs/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks. - render: bash + label: "Logs:" + description: "Please take a look at the [official documentation](https://docs.mailcow.email/troubleshooting/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks." + render: text validations: required: true - type: textarea attributes: - label: Steps to reproduce - description: Please describe the steps to reproduce the bug. Screenshots can be added, if helpful. + label: "Steps to reproduce:" + description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful." + render: text placeholder: |- 1. ... 2. ... 3. ... validations: required: true - - type: textarea + - type: markdown attributes: - label: System information - description: In this stage we would kindly ask you to attach general system information about your setup. - value: |- - | Question | Answer | - | --- | --- | - | My operating system | I_DO_REPLY_HERE | - | Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE | - | Virtualization technology (KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported** | I_DO_REPLY_HERE | - | Server/VM specifications (Memory, CPU Cores) | I_DO_REPLY_HERE | - | Docker version (`docker version`) | I_DO_REPLY_HERE | - | docker-compose version (`docker-compose version`) | I_DO_REPLY_HERE | - | mailcow version (```git describe --tags `git rev-list --tags --max-count=1` ```) | I_DO_REPLY_HERE | - | Reverse proxy (custom solution) | I_DO_REPLY_HERE | - - Output of `git diff origin/master`, any other changes to the code? If so, **please post them**: - ``` - YOUR OUTPUT GOES HERE - ``` - - All third-party firewalls and custom iptables rules are unsupported. **Please check the Docker docs about how to use Docker with your own ruleset**. Nevertheless, iptabels output can help us to help you: - iptables -L -vn: - ``` - YOUR OUTPUT GOES HERE - ``` - - ip6tables -L -vn: - ``` - YOUR OUTPUT GOES HERE - ``` - - iptables -L -vn -t nat: - ``` - YOUR OUTPUT GOES HERE - ``` - - ip6tables -L -vn -t nat: - ``` - YOUR OUTPUT GOES HERE - ``` - - DNS problems? Please run `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network) and post the output: - ``` - YOUR OUTPUT GOES HERE - ``` + value: | + ## System information + ### In this stage we would kindly ask you to attach general system information about your setup. + - type: dropdown + attributes: + label: "Which branch are you using?" + description: "#### `git rev-parse --abbrev-ref HEAD`" + multiple: false + options: + - master + - nightly + validations: + required: true + - type: input + attributes: + label: "Operating System:" + placeholder: "e.g. Ubuntu 22.04 LTS" + validations: + required: true + - type: input + attributes: + label: "Server/VM specifications:" + placeholder: "Memory, CPU Cores" + validations: + required: true + - type: input + attributes: + label: "Is Apparmor, SELinux or similar active?" + placeholder: "yes/no" + validations: + required: true + - type: input + attributes: + label: "Virtualization technology:" + placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**" + validations: + required: true + - type: input + attributes: + label: "Docker version:" + description: "#### `docker version`" + placeholder: "20.10.21" + validations: + required: true + - type: input + attributes: + label: "docker-compose version or docker compose version:" + description: "#### `docker-compose version` or `docker compose version`" + placeholder: "v2.12.2" + validations: + required: true + - type: input + attributes: + label: "mailcow version:" + description: "#### ```git describe --tags `git rev-list --tags --max-count=1` ```" + placeholder: "2022-08" + validations: + required: true + - type: input + attributes: + label: "Reverse proxy:" + placeholder: "e.g. Nginx/Traefik" + validations: + required: true + - type: textarea + attributes: + label: "Logs of git diff:" + description: "#### Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:" + render: text + validations: + required: true + - type: textarea + attributes: + label: "Logs of iptables -L -vn:" + description: "#### Output of `iptables -L -vn`" + render: text + validations: + required: true + - type: textarea + attributes: + label: "Logs of ip6tables -L -vn:" + description: "#### Output of `ip6tables -L -vn`" + render: text + validations: + required: true + - type: textarea + attributes: + label: "Logs of iptables -L -vn -t nat:" + description: "#### Output of `iptables -L -vn -t nat`" + render: text + validations: + required: true + - type: textarea + attributes: + label: "Logs of ip6tables -L -vn -t nat:" + description: "#### Output of `ip6tables -L -vn -t nat`" + render: text + validations: + required: true + - type: textarea + attributes: + label: "DNS check:" + description: "#### Output of `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network)" + render: text validations: required: true From 05181f188856d0ad6ae4220c7c6a59453c76a7ea Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 15 Nov 2022 19:44:07 +0100 Subject: [PATCH 012/170] Update issue template --- .github/ISSUE_TEMPLATE/Bug_report.yml | 166 ++++++++++++++++++-------- 1 file changed, 114 insertions(+), 52 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/Bug_report.yml b/.github/ISSUE_TEMPLATE/Bug_report.yml index 6134a9ad..2fe7082c 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.yml +++ b/.github/ISSUE_TEMPLATE/Bug_report.yml @@ -7,8 +7,8 @@ body: label: Contribution guidelines description: Please read the contribution guidelines before proceeding. options: - - label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree - required: true + - label: I've read the [contribution guidelines](https://github.com/mailcow/mailcow-dockerized/blob/master/CONTRIBUTING.md) and wholeheartedly agree + required: true - type: checkboxes attributes: label: I've found a bug and checked that ... @@ -26,70 +26,132 @@ body: attributes: label: Description description: Please provide a brief description of the bug in 1-2 sentences. If applicable, add screenshots to help explain your problem. Very useful for bugs in mailcow UI. + render: text validations: required: true - type: textarea attributes: - label: Logs - description: Please take a look at the [official documentation](https://mailcow.github.io/mailcow-dockerized-docs/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks. - render: bash + label: "Logs:" + description: "Please take a look at the [official documentation](https://docs.mailcow.email/troubleshooting/debug-logs/) and post the last few lines of logs, when the error occurs. For example, docker container logs of affected containers. This will be automatically formatted into code, so no need for backticks." + render: text validations: required: true - type: textarea attributes: - label: Steps to reproduce - description: Please describe the steps to reproduce the bug. Screenshots can be added, if helpful. + label: "Steps to reproduce:" + description: "Please describe the steps to reproduce the bug. Screenshots can be added, if helpful." + render: text placeholder: |- 1. ... 2. ... 3. ... validations: required: true - - type: textarea + - type: markdown attributes: - label: System information - description: In this stage we would kindly ask you to attach general system information about your setup. - value: |- - | Question | Answer | - | --- | --- | - | My operating system | I_DO_REPLY_HERE | - | Is Apparmor, SELinux or similar active? | I_DO_REPLY_HERE | - | Virtualization technology (KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported** | I_DO_REPLY_HERE | - | Server/VM specifications (Memory, CPU Cores) | I_DO_REPLY_HERE | - | Docker version (`docker version`) | I_DO_REPLY_HERE | - | docker-compose version (`docker-compose version`) | I_DO_REPLY_HERE | - | mailcow version (```git describe --tags `git rev-list --tags --max-count=1` ```) | I_DO_REPLY_HERE | - | Reverse proxy (custom solution) | I_DO_REPLY_HERE | - - Output of `git diff origin/master`, any other changes to the code? If so, **please post them**: - ``` - YOUR OUTPUT GOES HERE - ``` - - All third-party firewalls and custom iptables rules are unsupported. **Please check the Docker docs about how to use Docker with your own ruleset**. Nevertheless, iptabels output can help us to help you: - iptables -L -vn: - ``` - YOUR OUTPUT GOES HERE - ``` - - ip6tables -L -vn: - ``` - YOUR OUTPUT GOES HERE - ``` - - iptables -L -vn -t nat: - ``` - YOUR OUTPUT GOES HERE - ``` - - ip6tables -L -vn -t nat: - ``` - YOUR OUTPUT GOES HERE - ``` - - DNS problems? Please run `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network) and post the output: - ``` - YOUR OUTPUT GOES HERE - ``` + value: | + ## System information + ### In this stage we would kindly ask you to attach general system information about your setup. + - type: dropdown + attributes: + label: "Which branch are you using?" + description: "#### `git rev-parse --abbrev-ref HEAD`" + multiple: false + options: + - master + - nightly + validations: + required: true + - type: input + attributes: + label: "Operating System:" + placeholder: "e.g. Ubuntu 22.04 LTS" + validations: + required: true + - type: input + attributes: + label: "Server/VM specifications:" + placeholder: "Memory, CPU Cores" + validations: + required: true + - type: input + attributes: + label: "Is Apparmor, SELinux or similar active?" + placeholder: "yes/no" + validations: + required: true + - type: input + attributes: + label: "Virtualization technology:" + placeholder: "KVM, VMware, Xen, etc - **LXC and OpenVZ are not supported**" + validations: + required: true + - type: input + attributes: + label: "Docker version:" + description: "#### `docker version`" + placeholder: "20.10.21" + validations: + required: true + - type: input + attributes: + label: "docker-compose version or docker compose version:" + description: "#### `docker-compose version` or `docker compose version`" + placeholder: "v2.12.2" + validations: + required: true + - type: input + attributes: + label: "mailcow version:" + description: "#### ```git describe --tags `git rev-list --tags --max-count=1` ```" + placeholder: "2022-08" + validations: + required: true + - type: input + attributes: + label: "Reverse proxy:" + placeholder: "e.g. Nginx/Traefik" + validations: + required: true + - type: textarea + attributes: + label: "Logs of git diff:" + description: "#### Output of `git diff origin/master`, any other changes to the code? If so, **please post them**:" + render: text + validations: + required: true + - type: textarea + attributes: + label: "Logs of iptables -L -vn:" + description: "#### Output of `iptables -L -vn`" + render: text + validations: + required: true + - type: textarea + attributes: + label: "Logs of ip6tables -L -vn:" + description: "#### Output of `ip6tables -L -vn`" + render: text + validations: + required: true + - type: textarea + attributes: + label: "Logs of iptables -L -vn -t nat:" + description: "#### Output of `iptables -L -vn -t nat`" + render: text + validations: + required: true + - type: textarea + attributes: + label: "Logs of ip6tables -L -vn -t nat:" + description: "#### Output of `ip6tables -L -vn -t nat`" + render: text + validations: + required: true + - type: textarea + attributes: + label: "DNS check:" + description: "#### Output of `docker exec -it $(docker ps -qf name=acme-mailcow) dig +short stackoverflow.com @172.22.1.254` (set the IP accordingly, if you changed the internal mailcow network)" + render: text validations: required: true From a4eb6d5f1b66be037df2fa5e08911a111fe45869 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 16 Nov 2022 18:15:45 +0100 Subject: [PATCH 013/170] Update Release Tweet action --- .github/workflows/tweet-trigger-publish-release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tweet-trigger-publish-release.yml b/.github/workflows/tweet-trigger-publish-release.yml index 82f1dc3a..6f1b448a 100644 --- a/.github/workflows/tweet-trigger-publish-release.yml +++ b/.github/workflows/tweet-trigger-publish-release.yml @@ -4,9 +4,11 @@ on: types: [published] jobs: - build: + tweet: runs-on: ubuntu-latest steps: + - name: "Get Release Tag" + run: curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq '.tag_name' | sed 's/"//g' >> $RELEASE_TAG - name: Tweet-trigger-publish-release uses: mugi111/tweet-trigger-release@v1.1 with: @@ -14,4 +16,4 @@ jobs: consumer_secret: ${{ secrets.CONSUMER_SECRET }} access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }} access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }} - tweet_body: 'A new mailcow-dockerized Release has been Released on GitHub! Checkout our GitHub Page for the latest Release: github.com/mailcow/mailcow-dockerized/releases/latest' + tweet_body: '$RELEASE_TAG is here! Checkout the GitHub Page for changelog regarding the $RELEASE_TAG Release: github.com/mailcow/mailcow-dockerized/releases/tag/$RELEASE_TAG' From 3236a10cf5ca658530ae24ea7b6f8a4486f4e1af Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 16 Nov 2022 18:19:12 +0100 Subject: [PATCH 014/170] Updated tweet action (again) --- .github/workflows/tweet-trigger-publish-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tweet-trigger-publish-release.yml b/.github/workflows/tweet-trigger-publish-release.yml index 6f1b448a..c5d247e9 100644 --- a/.github/workflows/tweet-trigger-publish-release.yml +++ b/.github/workflows/tweet-trigger-publish-release.yml @@ -7,6 +7,8 @@ jobs: tweet: runs-on: ubuntu-latest steps: + - name: "Install jq" + run: apt update && apt install jq --no-install-recommends -y - name: "Get Release Tag" run: curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq '.tag_name' | sed 's/"//g' >> $RELEASE_TAG - name: Tweet-trigger-publish-release From 17f3cc3ad8502beaeb271398d6886590d133e185 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 16 Nov 2022 18:34:22 +0100 Subject: [PATCH 015/170] Optimized/Fixed Tweet action --- .github/workflows/tweet-trigger-publish-release.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tweet-trigger-publish-release.yml b/.github/workflows/tweet-trigger-publish-release.yml index c5d247e9..accfe1af 100644 --- a/.github/workflows/tweet-trigger-publish-release.yml +++ b/.github/workflows/tweet-trigger-publish-release.yml @@ -7,10 +7,10 @@ jobs: tweet: runs-on: ubuntu-latest steps: - - name: "Install jq" - run: apt update && apt install jq --no-install-recommends -y - name: "Get Release Tag" - run: curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq '.tag_name' | sed 's/"//g' >> $RELEASE_TAG + run: | + RELEASE_TAG=$(curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq -r '.tag_name') + echo "RELEASE_TAG=$RELEASE_TAG" >> $GITHUB_ENV - name: Tweet-trigger-publish-release uses: mugi111/tweet-trigger-release@v1.1 with: @@ -19,3 +19,5 @@ jobs: access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }} access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }} tweet_body: '$RELEASE_TAG is here! Checkout the GitHub Page for changelog regarding the $RELEASE_TAG Release: github.com/mailcow/mailcow-dockerized/releases/tag/$RELEASE_TAG' + env: + RELEASE_TAG: ${{ env.RELEASE_TAG }} From a46db9e0df91b45cc44311f617421f780dbe321c Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 16 Nov 2022 18:39:37 +0100 Subject: [PATCH 016/170] Fixed typo in tweet action --- .github/workflows/tweet-trigger-publish-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tweet-trigger-publish-release.yml b/.github/workflows/tweet-trigger-publish-release.yml index accfe1af..10e19bdd 100644 --- a/.github/workflows/tweet-trigger-publish-release.yml +++ b/.github/workflows/tweet-trigger-publish-release.yml @@ -20,4 +20,4 @@ jobs: access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }} tweet_body: '$RELEASE_TAG is here! Checkout the GitHub Page for changelog regarding the $RELEASE_TAG Release: github.com/mailcow/mailcow-dockerized/releases/tag/$RELEASE_TAG' env: - RELEASE_TAG: ${{ env.RELEASE_TAG }} + RELEASE_TAG: ${{ env.RELEASE_TAG }} From 046e6589841830a668552e8be9508293c8f2c9cd Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 16 Nov 2022 18:42:20 +0100 Subject: [PATCH 017/170] Use @MAGICCC Version of action --- .github/workflows/tweet-trigger-publish-release.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/tweet-trigger-publish-release.yml b/.github/workflows/tweet-trigger-publish-release.yml index 10e19bdd..daebfe53 100644 --- a/.github/workflows/tweet-trigger-publish-release.yml +++ b/.github/workflows/tweet-trigger-publish-release.yml @@ -10,7 +10,6 @@ jobs: - name: "Get Release Tag" run: | RELEASE_TAG=$(curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq -r '.tag_name') - echo "RELEASE_TAG=$RELEASE_TAG" >> $GITHUB_ENV - name: Tweet-trigger-publish-release uses: mugi111/tweet-trigger-release@v1.1 with: @@ -18,6 +17,4 @@ jobs: consumer_secret: ${{ secrets.CONSUMER_SECRET }} access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }} access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }} - tweet_body: '$RELEASE_TAG is here! Checkout the GitHub Page for changelog regarding the $RELEASE_TAG Release: github.com/mailcow/mailcow-dockerized/releases/tag/$RELEASE_TAG' - env: - RELEASE_TAG: ${{ env.RELEASE_TAG }} + tweet_body: '$RELEASE_TAG is here! Checkout the GitHub Page for changelog regarding the $RELEASE_TAG Release: github.com/mailcow/mailcow-dockerized/releases/tag/$RELEASE_TAG' \ No newline at end of file From 1e672ae349e326c2c058060fada166230c32042a Mon Sep 17 00:00:00 2001 From: Niklas Meyer <62480600+DerLinkman@users.noreply.github.com> Date: Thu, 17 Nov 2022 20:49:13 +0100 Subject: [PATCH 018/170] Update FUNDING.yml --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index befe7db7..11402129 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://mailcow.github.io/mailcow-dockerized-docs/#help-mailcow +custom: ["https://www.servercow.de/mailcow?lang=en#sal"] From e82f3b39755d6bc20ebd0fe5c316028bf7b8f1ea Mon Sep 17 00:00:00 2001 From: bluewalk Date: Thu, 17 Nov 2022 14:30:06 +0100 Subject: [PATCH 019/170] Added SENDER_ADDRESS and SENDER_NAME as variables for messages --- data/conf/rspamd/local.d/metadata_exporter.conf | 3 +-- data/conf/rspamd/meta_exporter/pushover.php | 11 +++++++++-- data/web/templates/edit/mailbox.twig | 2 +- data/web/templates/user/Pushover.twig | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/data/conf/rspamd/local.d/metadata_exporter.conf b/data/conf/rspamd/local.d/metadata_exporter.conf index 47373d99..daaa79b4 100644 --- a/data/conf/rspamd/local.d/metadata_exporter.conf +++ b/data/conf/rspamd/local.d/metadata_exporter.conf @@ -16,8 +16,7 @@ rules { backend = "http"; url = "http://nginx:9081/pushover.php"; selector = "mailcow_rcpt"; - # Only return msgid, do not parse the full message - formatter = "msgid"; + formatter = "json"; meta_headers = true; } } diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index a5e83343..974a282d 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -65,6 +65,13 @@ if (is_array($symbols_array)) { } } +$json = json_decode(file_get_contents('php://input')); + +$sender_address = $json->header_from ; +if (preg_match('/[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)/i', $sender, $matches)) + $sender_address = $matches[0]; +$sender_name = trim(str_replace('<' . $email . '>', '', $from)); + $rcpt_final_mailboxes = array(); // Loop through all rcpts @@ -229,9 +236,9 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) { $post_fields = array( "token" => $api_data['token'], "user" => $api_data['key'], - "title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $title)), + "title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}'), array($subject, $sender, $sender_name, $sender_address), $title)), "priority" => $priority, - "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}'), array($subject, $sender), $text)) + "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}'), array($subject, $sender, $sender_name, $sender_address), $text)) ); if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) { $post_fields['expire'] = 600; diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index e1c3e883..d4154292 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -275,7 +275,7 @@

{{ lang.user.pushover_info|format(mailbox)|raw }}

-

{{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}

+

{{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}

diff --git a/data/web/templates/user/Pushover.twig b/data/web/templates/user/Pushover.twig index 096655cb..8a6755a8 100644 --- a/data/web/templates/user/Pushover.twig +++ b/data/web/templates/user/Pushover.twig @@ -9,7 +9,7 @@

{{ lang.user.pushover_info|format(mailcow_cc_username)|raw }}

-

{{ lang.user.pushover_vars|raw }}: {SUBJECT}, {SENDER}

+

{{ lang.user.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}

From 65c74c75c74c1a626b8d8c60921a6dcb689e8f4a Mon Sep 17 00:00:00 2001 From: bluewalk Date: Thu, 17 Nov 2022 14:30:06 +0100 Subject: [PATCH 020/170] Added SENDER_ADDRESS and SENDER_NAME as variables for messages --- data/conf/rspamd/meta_exporter/pushover.php | 11 ++++--- data/web/api/openapi.yaml | 5 ++++ data/web/inc/functions.pushover.inc.php | 4 ++- data/web/inc/init_db.inc.php | 1 + data/web/lang/lang.en-gb.json | 2 ++ data/web/lang/lang.nl-nl.json | 2 ++ data/web/templates/edit/mailbox.twig | 30 +++++++++++++++++++ data/web/templates/user/Pushover.twig | 32 ++++++++++++++++++++- 8 files changed, 81 insertions(+), 6 deletions(-) diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index 974a282d..8db4d3d8 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -67,10 +67,12 @@ if (is_array($symbols_array)) { $json = json_decode(file_get_contents('php://input')); -$sender_address = $json->header_from ; -if (preg_match('/[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)/i', $sender, $matches)) +$sender_address = $json->header_from[0]; +$sender_name = '-'; +if (preg_match('/[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)/i', $sender_address, $matches)) { $sender_address = $matches[0]; -$sender_name = trim(str_replace('<' . $email . '>', '', $from)); + $sender_name = trim(str_replace('<' . $sender_address . '>', '', $json->header_from[0])); +} $rcpt_final_mailboxes = array(); @@ -238,7 +240,8 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) { "user" => $api_data['key'], "title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}'), array($subject, $sender, $sender_name, $sender_address), $title)), "priority" => $priority, - "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}'), array($subject, $sender, $sender_name, $sender_address), $text)) + "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}'), array($subject, $sender, $sender_name, $sender_address), $text)), + "sound" => $attributes['sound'] ?? "pushover" ); if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) { $post_fields['expire'] = 600; diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index c23380f1..6310aa58 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -3349,6 +3349,7 @@ paths: evaluate_x_prio: "0" key: 21e8918e1jksdjcpis712 only_x_prio: "0" + sound: "pushover" senders: "" senders_regex: "" text: "" @@ -3392,6 +3393,7 @@ paths: evaluate_x_prio: "0" key: 21e8918e1jksdjcpis712 only_x_prio: "0" + sound: "pushover" senders: "" senders_regex: "" text: "" @@ -3413,6 +3415,9 @@ paths: only_x_prio: description: Only send push for prio mails type: number + sound: + description: Set notification sound + type: string senders: description: Only send push for emails from these senders type: string diff --git a/data/web/inc/functions.pushover.inc.php b/data/web/inc/functions.pushover.inc.php index 74e8bb1c..5393c0d5 100644 --- a/data/web/inc/functions.pushover.inc.php +++ b/data/web/inc/functions.pushover.inc.php @@ -51,6 +51,7 @@ function pushover($_action, $_data = null) { $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; $evaluate_x_prio = (isset($_data['evaluate_x_prio'])) ? intval($_data['evaluate_x_prio']) : $is_now['evaluate_x_prio']; $only_x_prio = (isset($_data['only_x_prio'])) ? intval($_data['only_x_prio']) : $is_now['only_x_prio']; + $sound = (isset($_data['sound'])) ? $_data['sound'] : $is_now['sound']; } else { $_SESSION['return'][] = array( @@ -101,7 +102,8 @@ function pushover($_action, $_data = null) { $po_attributes = json_encode( array( 'evaluate_x_prio' => strval(intval($evaluate_x_prio)), - 'only_x_prio' => strval(intval($only_x_prio)) + 'only_x_prio' => strval(intval($only_x_prio)), + 'sound' => strval($sound) ) ); $stmt = $pdo->prepare("REPLACE INTO `pushover` (`username`, `key`, `attributes`, `senders_regex`, `senders`, `token`, `title`, `text`, `active`) diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index b47bd5c2..48db1a62 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -1264,6 +1264,7 @@ function init_db_schema() { $pdo->query("UPDATE `pushover` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.evaluate_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.evaluate_x_prio') IS NULL;"); $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.only_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.only_x_prio') IS NULL;"); + $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.sound', \"0\") WHERE JSON_VALUE(`attributes`, '$.sound') IS NULL;"); // mailbox $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.passwd_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.passwd_update') IS NULL;"); diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 0a384071..260997de 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -574,6 +574,7 @@ "pushover_sender_regex": "Consider the following sender regex", "pushover_text": "Notification text", "pushover_title": "Notification title", + "pushover_sound": "Sound", "pushover_vars": "When no sender filter is defined, all mails will be considered.
Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.
Useable variables for text and title (please take note of data protection policies)", "pushover_verify": "Verify credentials", "quota_mb": "Quota (MiB)", @@ -1097,6 +1098,7 @@ "pushover_sender_regex": "Match senders by the following regex", "pushover_text": "Notification text", "pushover_title": "Notification title", + "pushover_sound": "Sound", "pushover_vars": "When no sender filter is defined, all mails will be considered.
Regex filters as well as exact sender checks can be defined individually and will be considered sequentially. They do not depend on each other.
Useable variables for text and title (please take note of data protection policies)", "pushover_verify": "Verify credentials", "q_add_header": "Junk folder", diff --git a/data/web/lang/lang.nl-nl.json b/data/web/lang/lang.nl-nl.json index ecfee43d..af8e9834 100644 --- a/data/web/lang/lang.nl-nl.json +++ b/data/web/lang/lang.nl-nl.json @@ -536,6 +536,7 @@ "pushover_sender_regex": "Uitsluitend een afzender met de volgende regex", "pushover_text": "Meldingstekst ({SUBJECT} zal worden vervangen door het onderwerp)", "pushover_title": "Meldingstitel", + "pushover_sound": "Geluid", "pushover_vars": "Wanneer er geen afzenders zijn uitgesloten zullen alle mails doorkomen.
Regex-filters en afzendercontroles kunnen individueel worden ingesteld en zullen in volgorde worden verwerkt. Ze zijn niet afhankelijk van elkaar.
Bruikbare variabelen voor tekst en titel (neem het gegevensbeschermingsbeleid in acht)", "pushover_verify": "Verifieer aanmeldingsgegevens", "quota_mb": "Quota (MiB)", @@ -1002,6 +1003,7 @@ "pushover_sender_regex": "Uitsluitend een afzender met de volgende regex", "pushover_text": "Meldingstekst ({SUBJECT} zal worden vervangen door het onderwerp)", "pushover_title": "Meldingstitel", + "pushover_sound": "Geluid", "pushover_vars": "Wanneer er geen afzenders zijn uitgesloten zullen alle mails doorkomen.
Regex-filters en afzendercontroles kunnen individueel worden ingesteld en zullen in volgorde worden verwerkt. Ze zijn niet afhankelijk van elkaar.
Bruikbare variabelen voor tekst en titel (let op het gegevensbeschermingsbeleid)", "pushover_verify": "Verifieer aanmeldingsgegevens", "q_add_header": "Spamfolder", diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index d4154292..f0154584 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -308,6 +308,36 @@
+
+
+
+ +
+
diff --git a/data/web/templates/user/Pushover.twig b/data/web/templates/user/Pushover.twig index 8a6755a8..a1867a8b 100644 --- a/data/web/templates/user/Pushover.twig +++ b/data/web/templates/user/Pushover.twig @@ -9,7 +9,7 @@

{{ lang.user.pushover_info|format(mailcow_cc_username)|raw }}

-

{{ lang.user.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}

+

{{ lang.user.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}

@@ -42,6 +42,36 @@
+
+
+
+ +
+
From 57a5a9baeb5758d441fbd6c80a00c9d3c94af153 Mon Sep 17 00:00:00 2001 From: bluewalk Date: Thu, 17 Nov 2022 21:14:44 +0100 Subject: [PATCH 021/170] Updated DB version and make sure default sound is "pushover" when null --- data/web/inc/init_db.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 48db1a62..9abd4485 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "25072022_2300"; + $db_version = "17112022_2115"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -1264,7 +1264,7 @@ function init_db_schema() { $pdo->query("UPDATE `pushover` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.evaluate_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.evaluate_x_prio') IS NULL;"); $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.only_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.only_x_prio') IS NULL;"); - $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.sound', \"0\") WHERE JSON_VALUE(`attributes`, '$.sound') IS NULL;"); + $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.sound', \"pushover\") WHERE JSON_VALUE(`attributes`, '$.sound') IS NULL;"); // mailbox $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.passwd_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.passwd_update') IS NULL;"); From fd14c51f850beb893624808b468233f874526b84 Mon Sep 17 00:00:00 2001 From: bluewalk Date: Fri, 18 Nov 2022 17:28:10 +0100 Subject: [PATCH 022/170] Removed regex as we have the address from the header --- data/conf/rspamd/meta_exporter/pushover.php | 26 +++++++++------------ 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index 8db4d3d8..ffc826a7 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -48,12 +48,12 @@ if (!function_exists('getallheaders')) { $headers = getallheaders(); -$qid = $headers['X-Rspamd-Qid']; -$rcpts = $headers['X-Rspamd-Rcpt']; -$sender = $headers['X-Rspamd-From']; -$ip = $headers['X-Rspamd-Ip']; -$subject = $headers['X-Rspamd-Subject']; -$priority = 0; +$qid = $headers['X-Rspamd-Qid']; +$rcpts = $headers['X-Rspamd-Rcpt']; +$sender_address = $headers['X-Rspamd-From']; +$ip = $headers['X-Rspamd-Ip']; +$subject = $headers['X-Rspamd-Subject']; +$priority = 0; $symbols_array = json_decode($headers['X-Rspamd-Symbols'], true); if (is_array($symbols_array)) { @@ -67,12 +67,8 @@ if (is_array($symbols_array)) { $json = json_decode(file_get_contents('php://input')); -$sender_address = $json->header_from[0]; -$sender_name = '-'; -if (preg_match('/[a-zA-Z0-9.!#$%&’*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)/i', $sender_address, $matches)) { - $sender_address = $matches[0]; - $sender_name = trim(str_replace('<' . $sender_address . '>', '', $json->header_from[0])); -} +$sender = $json->header_from[0]; +$sender_name = trim(str_replace('<' . $sender_address . '>', '', $sender)); $rcpt_final_mailboxes = array(); @@ -217,18 +213,18 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) { } else { if (!empty($senders)) { - if (in_array($sender, $senders)) { + if (in_array($sender_address, $senders)) { $sender_validated = true; } } if (!empty($senders_regex) && $sender_validated !== true) { - if (preg_match($senders_regex, $sender)) { + if (preg_match($senders_regex, $sender_address)) { $sender_validated = true; } } } if ($sender_validated === false) { - error_log("NOTIFY: pushover pipe: skipping unwanted sender " . $sender); + error_log("NOTIFY: pushover pipe: skipping unwanted sender " . $sender_address); continue; } if ($attributes['only_x_prio'] == "1" && $priority == 0) { From d8e314db1a34ef29996f7575b66df894351685e3 Mon Sep 17 00:00:00 2001 From: bluewalk Date: Sat, 19 Nov 2022 15:32:48 +0100 Subject: [PATCH 023/170] Fixed issue with subdomain senders + added TO variable and allow new lines in text using \n --- data/conf/rspamd/meta_exporter/pushover.php | 33 ++++++++++++--------- data/web/templates/edit/mailbox.twig | 2 +- data/web/templates/user/Pushover.twig | 2 +- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index ffc826a7..2f2206ea 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -47,13 +47,15 @@ if (!function_exists('getallheaders')) { } $headers = getallheaders(); +$json_body = json_decode(file_get_contents('php://input')); -$qid = $headers['X-Rspamd-Qid']; -$rcpts = $headers['X-Rspamd-Rcpt']; -$sender_address = $headers['X-Rspamd-From']; -$ip = $headers['X-Rspamd-Ip']; -$subject = $headers['X-Rspamd-Subject']; -$priority = 0; +$qid = $headers['X-Rspamd-Qid']; +$rcpts = $headers['X-Rspamd-Rcpt']; +$sender = $headers['X-Rspamd-From']; +$ip = $headers['X-Rspamd-Ip']; +$subject = $headers['X-Rspamd-Subject']; +$priority = 0; +$to = $json_body->header_to[0]; $symbols_array = json_decode($headers['X-Rspamd-Symbols'], true); if (is_array($symbols_array)) { @@ -65,10 +67,13 @@ if (is_array($symbols_array)) { } } -$json = json_decode(file_get_contents('php://input')); +$sender_address = $json_body->header_from[0]; +$sender_name = '-'; -$sender = $json->header_from[0]; -$sender_name = trim(str_replace('<' . $sender_address . '>', '', $sender)); +if (preg_match('/(?.*?)<(?
.*?)>/i', $sender_address, $matches)) { + $sender_address = $matches['address']; + $sender_name = trim(trim($matches['name']), '"\' '); +} $rcpt_final_mailboxes = array(); @@ -213,18 +218,18 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) { } else { if (!empty($senders)) { - if (in_array($sender_address, $senders)) { + if (in_array($sender, $senders)) { $sender_validated = true; } } if (!empty($senders_regex) && $sender_validated !== true) { - if (preg_match($senders_regex, $sender_address)) { + if (preg_match($senders_regex, $sender)) { $sender_validated = true; } } } if ($sender_validated === false) { - error_log("NOTIFY: pushover pipe: skipping unwanted sender " . $sender_address); + error_log("NOTIFY: pushover pipe: skipping unwanted sender " . $sender); continue; } if ($attributes['only_x_prio'] == "1" && $priority == 0) { @@ -234,9 +239,9 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) { $post_fields = array( "token" => $api_data['token'], "user" => $api_data['key'], - "title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}'), array($subject, $sender, $sender_name, $sender_address), $title)), + "title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO}'), array($subject, $sender, $sender_name, $sender_address, $to), $title)), "priority" => $priority, - "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}'), array($subject, $sender, $sender_name, $sender_address), $text)), + "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO}', '\n'), array($subject, $sender, $sender_name, $sender_address, $to, PHP_EOL), $text)), "sound" => $attributes['sound'] ?? "pushover" ); if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) { diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index f0154584..f2e80c96 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -275,7 +275,7 @@

{{ lang.user.pushover_info|format(mailbox)|raw }}

-

{{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}

+

{{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}, {TO}

diff --git a/data/web/templates/user/Pushover.twig b/data/web/templates/user/Pushover.twig index a1867a8b..ea1d2c16 100644 --- a/data/web/templates/user/Pushover.twig +++ b/data/web/templates/user/Pushover.twig @@ -9,7 +9,7 @@

{{ lang.user.pushover_info|format(mailcow_cc_username)|raw }}

-

{{ lang.user.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}

+

{{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}, {TO}

From 360bb6f30603f9bf3104e3fdc575159cc7729805 Mon Sep 17 00:00:00 2001 From: bluewalk Date: Sun, 20 Nov 2022 10:42:44 +0100 Subject: [PATCH 024/170] Split name and address for TO-variables --- data/conf/rspamd/meta_exporter/pushover.php | 21 ++++++++++++++++----- data/web/templates/edit/mailbox.twig | 2 +- data/web/templates/user/Pushover.twig | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/data/conf/rspamd/meta_exporter/pushover.php b/data/conf/rspamd/meta_exporter/pushover.php index 2f2206ea..4c8092d1 100644 --- a/data/conf/rspamd/meta_exporter/pushover.php +++ b/data/conf/rspamd/meta_exporter/pushover.php @@ -55,7 +55,6 @@ $sender = $headers['X-Rspamd-From']; $ip = $headers['X-Rspamd-Ip']; $subject = $headers['X-Rspamd-Subject']; $priority = 0; -$to = $json_body->header_to[0]; $symbols_array = json_decode($headers['X-Rspamd-Symbols'], true); if (is_array($symbols_array)) { @@ -69,10 +68,16 @@ if (is_array($symbols_array)) { $sender_address = $json_body->header_from[0]; $sender_name = '-'; - if (preg_match('/(?.*?)<(?
.*?)>/i', $sender_address, $matches)) { $sender_address = $matches['address']; - $sender_name = trim(trim($matches['name']), '"\' '); + $sender_name = trim($matches['name'], '"\' '); +} + +$to_address = $json_body->header_to[0]; +$to_name = '-'; +if (preg_match('/(?.*?)<(?
.*?)>/i', $to_address, $matches)) { + $to_address = $matches['address']; + $to_name = trim($matches['name'], '"\' '); } $rcpt_final_mailboxes = array(); @@ -239,9 +244,15 @@ foreach ($rcpt_final_mailboxes as $rcpt_final) { $post_fields = array( "token" => $api_data['token'], "user" => $api_data['key'], - "title" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO}'), array($subject, $sender, $sender_name, $sender_address, $to), $title)), + "title" => sprintf("%s", str_replace( + array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}'), + array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address), $title) + ), "priority" => $priority, - "message" => sprintf("%s", str_replace(array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO}', '\n'), array($subject, $sender, $sender_name, $sender_address, $to, PHP_EOL), $text)), + "message" => sprintf("%s", str_replace( + array('{SUBJECT}', '{SENDER}', '{SENDER_NAME}', '{SENDER_ADDRESS}', '{TO_NAME}', '{TO_ADDRESS}', '\n'), + array($subject, $sender, $sender_name, $sender_address, $to_name, $to_address, PHP_EOL), $text) + ), "sound" => $attributes['sound'] ?? "pushover" ); if ($attributes['evaluate_x_prio'] == "1" && $priority == 1) { diff --git a/data/web/templates/edit/mailbox.twig b/data/web/templates/edit/mailbox.twig index f2e80c96..9eb7c951 100644 --- a/data/web/templates/edit/mailbox.twig +++ b/data/web/templates/edit/mailbox.twig @@ -275,7 +275,7 @@

{{ lang.user.pushover_info|format(mailbox)|raw }}

-

{{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}, {TO}

+

{{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}, {TO_NAME}, {TO_ADDRESS}

diff --git a/data/web/templates/user/Pushover.twig b/data/web/templates/user/Pushover.twig index ea1d2c16..5bd6b1a4 100644 --- a/data/web/templates/user/Pushover.twig +++ b/data/web/templates/user/Pushover.twig @@ -9,7 +9,7 @@

{{ lang.user.pushover_info|format(mailcow_cc_username)|raw }}

-

{{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}, {TO}

+

{{ lang.edit.pushover_vars|raw }}: {SUBJECT}, {SENDER}, {SENDER_ADDRESS}, {SENDER_NAME}, {TO_NAME}, {TO_ADDRESS}

From 118cb1017a63281fb4dbef4e0959388cc9aacae6 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 22 Nov 2022 18:37:15 +0100 Subject: [PATCH 025/170] Add new action Build mailcow backup image --- .github/workflows/rebuild_backup_image.yml | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/rebuild_backup_image.yml diff --git a/.github/workflows/rebuild_backup_image.yml b/.github/workflows/rebuild_backup_image.yml new file mode 100644 index 00000000..120d68d9 --- /dev/null +++ b/.github/workflows/rebuild_backup_image.yml @@ -0,0 +1,34 @@ +name: Build mailcow backup image + +on: + schedule: + # At 00:00 on Sunday + - cron: "0 0 * * 0" + workflow_dispatch: # Allow to run workflow manually + +jobs: + docker_image_build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_USERNAME }} + password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + file: data/Dockerfiles/backup/Dockerfile + push: true + tags: mailcow/backup:latest From ff7102468ee0db49d866301aecc03690f69f1191 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 22 Nov 2022 18:38:38 +0100 Subject: [PATCH 026/170] [Helper] Backup and restore: Use latest tag for image --- helper-scripts/backup_and_restore.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helper-scripts/backup_and_restore.sh b/helper-scripts/backup_and_restore.sh index 1853f501..ee9f0202 100755 --- a/helper-scripts/backup_and_restore.sh +++ b/helper-scripts/backup_and_restore.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -DEBIAN_DOCKER_IMAGE="mailcow/backup:1.0" +DEBIAN_DOCKER_IMAGE="mailcow/backup:latest" if [[ ! -z ${MAILCOW_BACKUP_LOCATION} ]]; then BACKUP_LOCATION="${MAILCOW_BACKUP_LOCATION}" @@ -58,7 +58,7 @@ if ! [[ "${THREADS}" =~ ^[1-9]+$ ]] ; then echo "Thread input is not a number!" exit 1 elif [[ "${THREADS}" =~ ^[1-9]+$ ]] ; then - echo "Using ${THREADS} Thread(s) for this run." + echo "Using ${THREADS} Thread(s) for this run." echo "Notice: You can set the Thread count with the THREADS Variable before you run this script." fi @@ -181,7 +181,7 @@ function restore() { elif [ "${DOCKER_COMPOSE_VERSION}" == "standalone" ]; then COMPOSE_COMMAND="docker-compose" - + else echo -e "\e[31mCan not read DOCKER_COMPOSE_VERSION variable from mailcow.conf! Is your mailcow up to date? Exiting...\e[0m" exit 1 @@ -380,4 +380,4 @@ elif [[ ${1} == "restore" ]]; then done echo "Restoring ${FILE_SELECTION[${input_sel}]} from ${RESTORE_POINT}..." restore "${RESTORE_POINT}" ${FILE_SELECTION[${input_sel}]} -fi \ No newline at end of file +fi From f2f5e212f5c725c56b4703e5a45b85efe99a540d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20S?= Date: Wed, 23 Nov 2022 22:10:57 +0900 Subject: [PATCH 027/170] Fixy comment typo --- .../HAPROXY/docker-compose.override.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helper-scripts/docker-compose.override.yml.d/HAPROXY/docker-compose.override.yml b/helper-scripts/docker-compose.override.yml.d/HAPROXY/docker-compose.override.yml index 21d391b8..5009f4c9 100644 --- a/helper-scripts/docker-compose.override.yml.d/HAPROXY/docker-compose.override.yml +++ b/helper-scripts/docker-compose.override.yml.d/HAPROXY/docker-compose.override.yml @@ -1,6 +1,6 @@ ## ## Set haproxy_trusted_networks in Dovecots extra.conf! -#ä +## version: '2.1' services: From 524aba09644640dd41b54dee4946c5d9ef590a43 Mon Sep 17 00:00:00 2001 From: milkmaker Date: Fri, 25 Nov 2022 19:52:37 +0100 Subject: [PATCH 028/170] [Web] Updated lang.sk-sk.json (#4873) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lukáš Matula Co-authored-by: Lukáš Matula --- data/web/lang/lang.sk-sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/lang/lang.sk-sk.json b/data/web/lang/lang.sk-sk.json index 8bb1689e..1b702df8 100644 --- a/data/web/lang/lang.sk-sk.json +++ b/data/web/lang/lang.sk-sk.json @@ -106,7 +106,7 @@ "username": "Používateľské meno", "validate": "Overiť", "validation_success": "Úspešne overené", - "app_passwd_protocols": "Povolené protokoly" + "app_passwd_protocols": "Povolené protokoly k heslu aplikácie" }, "admin": { "access": "Prístup", From 73370de1f97cb363e5e32a7b625ba01185cc1183 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Wed, 30 Nov 2022 11:08:38 +0100 Subject: [PATCH 029/170] Update SOGo to 5.8.0 nightly --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 05d5d83b..607a1b55 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -168,7 +168,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.111 + image: mailcow/sogo:1.12 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From 86b67a9a7b33943161fff0821ee94cbe8641ff01 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 30 Nov 2022 17:13:39 +0100 Subject: [PATCH 030/170] Updated mailcow/sogo to 1.112 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 607a1b55..5dec4e3e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -168,7 +168,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.12 + image: mailcow/sogo:1.112 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From 0b00f15811c328dee74e3b9121282a19846de68b Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Wed, 30 Nov 2022 17:37:33 +0100 Subject: [PATCH 031/170] Added additional Check for Docker Hub --- update.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/update.sh b/update.sh index 69ada6b0..3eca2ea6 100755 --- a/update.sh +++ b/update.sh @@ -3,7 +3,7 @@ ############## Begin Function Section ############## check_online_status() { - CHECK_ONLINE_DOMAINS=('https://github.com') + CHECK_ONLINE_DOMAINS=('https://github.com' 'https://hub.docker.com') for domain in "${CHECK_ONLINE_DOMAINS[@]}"; do if timeout 3 curl --head --silent --output /dev/null ${domain}; then return 0 From 77f04d10c791ed4320ea915eefb43a9fc0c407db Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 1 Dec 2022 23:02:03 +0100 Subject: [PATCH 032/170] Update Base Images to Alpine 3.17 --- data/Dockerfiles/acme/Dockerfile | 2 +- data/Dockerfiles/dockerapi/Dockerfile | 2 +- data/Dockerfiles/netfilter/Dockerfile | 2 +- data/Dockerfiles/olefy/Dockerfile | 2 +- data/Dockerfiles/phpfpm/Dockerfile | 2 +- data/Dockerfiles/unbound/Dockerfile | 2 +- data/Dockerfiles/watchdog/Dockerfile | 2 +- docker-compose.yml | 14 +++++++------- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/data/Dockerfiles/acme/Dockerfile b/data/Dockerfiles/acme/Dockerfile index f5b7b56c..571c3d08 100644 --- a/data/Dockerfiles/acme/Dockerfile +++ b/data/Dockerfiles/acme/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index 41d4a78f..e7907531 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " diff --git a/data/Dockerfiles/netfilter/Dockerfile b/data/Dockerfiles/netfilter/Dockerfile index 621da149..bc707391 100644 --- a/data/Dockerfiles/netfilter/Dockerfile +++ b/data/Dockerfiles/netfilter/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " ENV XTABLES_LIBDIR /usr/lib/xtables diff --git a/data/Dockerfiles/olefy/Dockerfile b/data/Dockerfiles/olefy/Dockerfile index 889f84b4..10d63d02 100644 --- a/data/Dockerfiles/olefy/Dockerfile +++ b/data/Dockerfiles/olefy/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " WORKDIR /app diff --git a/data/Dockerfiles/phpfpm/Dockerfile b/data/Dockerfiles/phpfpm/Dockerfile index 38c68f70..93acb33f 100644 --- a/data/Dockerfiles/phpfpm/Dockerfile +++ b/data/Dockerfiles/phpfpm/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.1-fpm-alpine3.16 +FROM php:8.1-fpm-alpine3.17 LABEL maintainer "Andre Peters " ENV APCU_PECL 5.1.22 diff --git a/data/Dockerfiles/unbound/Dockerfile b/data/Dockerfiles/unbound/Dockerfile index 0b1cefe9..d9756d04 100644 --- a/data/Dockerfiles/unbound/Dockerfile +++ b/data/Dockerfiles/unbound/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " diff --git a/data/Dockerfiles/watchdog/Dockerfile b/data/Dockerfiles/watchdog/Dockerfile index 637c4680..654dea08 100644 --- a/data/Dockerfiles/watchdog/Dockerfile +++ b/data/Dockerfiles/watchdog/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "André Peters " # Installation diff --git a/docker-compose.yml b/docker-compose.yml index 621eb610..7f7c2428 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '2.1' services: unbound-mailcow: - image: mailcow/unbound:1.16 + image: mailcow/unbound:1.17 environment: - TZ=${TZ} volumes: @@ -106,7 +106,7 @@ services: - rspamd php-fpm-mailcow: - image: mailcow/phpfpm:1.80 + image: mailcow/phpfpm:1.81 command: "php-fpm -d date.timezone=${TZ} -d expose_php=0" depends_on: - redis-mailcow @@ -388,7 +388,7 @@ services: acme-mailcow: depends_on: - nginx-mailcow - image: mailcow/acme:1.82 + image: mailcow/acme:1.83 dns: - ${IPV4_NETWORK:-172.22.1}.254 environment: @@ -424,7 +424,7 @@ services: - acme netfilter-mailcow: - image: mailcow/netfilter:1.49 + image: mailcow/netfilter:1.50 stop_grace_period: 30s depends_on: - dovecot-mailcow @@ -447,7 +447,7 @@ services: - /lib/modules:/lib/modules:ro watchdog-mailcow: - image: mailcow/watchdog:1.96 + image: mailcow/watchdog:1.97 dns: - ${IPV4_NETWORK:-172.22.1}.254 tmpfs: @@ -509,7 +509,7 @@ services: - watchdog dockerapi-mailcow: - image: mailcow/dockerapi:1.42 + image: mailcow/dockerapi:1.43 security_opt: - label=disable restart: always @@ -543,7 +543,7 @@ services: - solr olefy-mailcow: - image: mailcow/olefy:1.10 + image: mailcow/olefy:1.11 restart: always environment: - TZ=${TZ} From 8614d63ace74804d8ab7a82ef82c29941c6963ef Mon Sep 17 00:00:00 2001 From: Schwindelhub <1729218+schwindelbub@users.noreply.github.com> Date: Sat, 3 Dec 2022 21:21:00 +0100 Subject: [PATCH 033/170] Update lang.de-de.json Corrected "Leerzeichen in Komposita". --- data/web/lang/lang.de-de.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 7167290f..e1a6e12f 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -265,8 +265,8 @@ "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", - "quota_notifications": "Quota Benachrichtigungen", - "quota_notifications_info": "Quota Benachrichtigungen werden an Mailboxen versendet, die 80 respektive 95 Prozent der zur Verfügung stehenden Quota überschreiten.", + "quota_notifications": "Quota-Benachrichtigungen", + "quota_notifications_info": "Quota-Benachrichtigungen werden an Mailboxen versendet, die 80 respektive 95 Prozent der zur Verfügung stehenden Quota überschreiten.", "quota_notifications_vars": "{{percent}} entspricht der aktuellen Quota in Prozent
{{username}} entspricht dem Mailbox-Namen", "r_active": "Aktive Restriktionen", "r_inactive": "Inaktive Restriktionen", From 299a342a62b8c9c57efe2799095c850a181ec743 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Thu, 8 Dec 2022 15:57:24 +0100 Subject: [PATCH 034/170] [Nextcloud] Update to 25 + purge fix (DB) --- helper-scripts/nextcloud.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/helper-scripts/nextcloud.sh b/helper-scripts/nextcloud.sh index 94bc997a..16311fc2 100755 --- a/helper-scripts/nextcloud.sh +++ b/helper-scripts/nextcloud.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +NEXTCLOUD_VER="25" for bin in curl dirmngr; do if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi @@ -40,7 +41,7 @@ if [[ ${NC_PURGE} == "y" ]]; then fi docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e \ - "$(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "SELECT IFNULL(GROUP_CONCAT('DROP TABLE ', TABLE_SCHEMA, '.', TABLE_NAME SEPARATOR ';'),'SELECT NULL;') FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'nc_%' AND TABLE_SCHEMA = '${DBNAME}';" -BN)" + "$(docker exec -it $(docker ps -f name=mysql-mailcow -q) mysql -uroot -p${DBROOT} -e "SELECT IFNULL(GROUP_CONCAT('DROP TABLE ', TABLE_SCHEMA, '.', TABLE_NAME SEPARATOR ';'),'SELECT NULL;') FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME LIKE 'oc_%' AND TABLE_SCHEMA = '${DBNAME}';" -BN)" docker exec -it $(docker ps -f name=redis-mailcow -q) /bin/sh -c ' cat < Date: Thu, 8 Dec 2022 16:09:20 +0100 Subject: [PATCH 035/170] [DockerAPI] Tagged as 2.0 (rewrite) --- data/Dockerfiles/dockerapi/Dockerfile | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/Dockerfiles/dockerapi/Dockerfile b/data/Dockerfiles/dockerapi/Dockerfile index f021b73e..97c3808c 100644 --- a/data/Dockerfiles/dockerapi/Dockerfile +++ b/data/Dockerfiles/dockerapi/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.16 +FROM alpine:3.17 LABEL maintainer "Andre Peters " diff --git a/docker-compose.yml b/docker-compose.yml index eb744f96..4f370796 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -510,7 +510,7 @@ services: - watchdog dockerapi-mailcow: - image: mailcow/dockerapi:1.44 + image: mailcow/dockerapi:2.0 security_opt: - label=disable restart: always From 6704377402138576e25b5daa062f7d5ba203987e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 9 Dec 2022 16:10:10 +0100 Subject: [PATCH 036/170] [Web] escape more html data --- data/web/js/build/014-mailcow.js | 2 +- data/web/js/site/mailbox.js | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/data/web/js/build/014-mailcow.js b/data/web/js/build/014-mailcow.js index 13bc2911..66da6076 100644 --- a/data/web/js/build/014-mailcow.js +++ b/data/web/js/build/014-mailcow.js @@ -1,7 +1,7 @@ $(document).ready(function() { // mailcow alert box generator window.mailcow_alert_box = function(message, type) { - msg = $('').text(message).text(); + msg = $('').text(escapeHtml(message)).text(); if (type == 'danger' || type == 'info') { auto_hide = 0; $('#' + localStorage.getItem("add_modal")).modal('show'); diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 8c98e922..ac481cea 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -441,6 +441,8 @@ jQuery(function($){ url: "/api/v1/get/domain/all", dataSrc: function(json){ $.each(json, function(i, item) { + item.domain_name = escapeHtml(item.domain_name); + item.aliases = item.aliases_in_domain + " / " + item.max_num_aliases_for_domain; item.mailboxes = item.mboxes_in_domain + " / " + item.max_num_mboxes_for_domain; item.quota = item.quota_used_in_domain + "/" + item.max_quota_for_domain + "/" + item.bytes_total; @@ -625,7 +627,6 @@ jQuery(function($){ type: "GET", url: "/api/v1/get/domain/template/all", dataSrc: function(json){ - console.log(json); $.each(json, function (i, item) { item.chkbox = ''; @@ -1582,7 +1583,6 @@ jQuery(function($){ type: "GET", url: "/api/v1/get/tls-policy-map/all", dataSrc: function(json){ - console.log(json); if (role !== "admin") return null; $.each(json, function (i, item) { @@ -1817,6 +1817,8 @@ jQuery(function($){ url: "/api/v1/get/alias-domain/all", dataSrc: function(json){ $.each(json, function (i, item) { + item.alias_domain = escapeHtml(item.alias_domain); + item.action = '
' + ' ' + lang.edit + '' + ' ' + lang.remove + '' + @@ -1904,7 +1906,7 @@ jQuery(function($){ } else { item.exclude = '' + escapeHtml(item.exclude) + ''; } - item.server_w_port = escapeHtml(item.user1) + '@' + item.host1 + ':' + item.port1; + item.server_w_port = escapeHtml(item.user1) + '@' + escapeHtml(item.host1) + ':' + escapeHtml(item.port1); item.action = '
' + ' ' + lang.edit + '' + ' ' + lang.remove + '' + @@ -2042,6 +2044,7 @@ jQuery(function($){ } else { item.active = '' + lang.inactive + ''; } + item.script_desc = escapeHtml(item.script_desc); item.script_data = '
' + escapeHtml(item.script_data) + '
' item.filter_type = '
' + item.filter_type.charAt(0).toUpperCase() + item.filter_type.slice(1).toLowerCase() + '
' item.action = '
' + From f4731eecdb029be7393177bc76c6e649e40320b2 Mon Sep 17 00:00:00 2001 From: DerLinkman Date: Mon, 12 Dec 2022 10:49:00 +0100 Subject: [PATCH 037/170] Cleanup + Language Fixes --- data/web/inc/functions.mailbox.inc.php | 27 ++++++++--------- data/web/js/site/mailbox.js | 30 ++++++++++++++----- data/web/lang/lang.de-de.json | 9 ++++++ data/web/lang/lang.en-gb.json | 1 + data/web/templates/mailbox.twig | 3 -- .../mailbox/tab-mailbox-defaults.twig | 13 -------- 6 files changed, 45 insertions(+), 38 deletions(-) delete mode 100644 data/web/templates/mailbox/tab-mailbox-defaults.twig diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 55c8d6bc..d67fa3e3 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -1420,11 +1420,11 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { // check attributes $attr = array(); $attr['tags'] = (isset($_data['tags'])) ? $_data['tags'] : array(); - $attr['max_num_aliases_for_domain'] = (isset($_data['max_num_aliases_for_domain'])) ? intval($_data['max_num_aliases_for_domain']) : 0; - $attr['max_num_mboxes_for_domain'] = (isset($_data['max_num_mboxes_for_domain'])) ? intval($_data['max_num_mboxes_for_domain']) : 0; - $attr['def_quota_for_mbox'] = (isset($_data['def_quota_for_mbox'])) ? intval($_data['def_quota_for_mbox']) * 1048576 : 0; - $attr['max_quota_for_mbox'] = (isset($_data['max_quota_for_mbox'])) ? intval($_data['max_quota_for_mbox']) * 1048576 : 0; - $attr['max_quota_for_domain'] = (isset($_data['max_quota_for_domain'])) ? intval($_data['max_quota_for_domain']) * 1048576 : 0; + $attr['max_num_aliases_for_domain'] = (!empty($_data['max_num_aliases_for_domain'])) ? intval($_data['max_num_aliases_for_domain']) : 400; + $attr['max_num_mboxes_for_domain'] = (!empty($_data['max_num_mboxes_for_domain'])) ? intval($_data['max_num_mboxes_for_domain']) : 10; + $attr['def_quota_for_mbox'] = (!empty($_data['def_quota_for_mbox'])) ? intval($_data['def_quota_for_mbox']) * 1048576 : 3072 * 1048576; + $attr['max_quota_for_mbox'] = (!empty($_data['max_quota_for_mbox'])) ? intval($_data['max_quota_for_mbox']) * 1048576 : 10240 * 1048576; + $attr['max_quota_for_domain'] = (!empty($_data['max_quota_for_domain'])) ? intval($_data['max_quota_for_domain']) * 1048576 : 10240 * 1048576; $attr['rl_frame'] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : "s"; $attr['rl_value'] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : ""; $attr['active'] = isset($_data['active']) ? intval($_data['active']) : 1; @@ -1435,7 +1435,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $attr['dkim_selector'] = (isset($_data['dkim_selector'])) ? $_data['dkim_selector'] : "dkim"; $attr['key_size'] = isset($_data['key_size']) ? intval($_data['key_size']) : 2048; - // save template $stmt = $pdo->prepare("INSERT INTO `templates` (`type`, `template`, `attributes`) VALUES (:type, :template, :attributes)"); @@ -4756,15 +4755,15 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ":id" => $id, ":type" => "domain", ":template" => "Default" - )); - } + )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), - 'msg' => 'template_removed' - ); - return true; + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => array('template_removed', htmlspecialchars($id)) + ); + return true; + } break; case 'alias': if (!is_array($_data['id'])) { diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 8c98e922..c36a9e7d 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -645,10 +645,18 @@ jQuery(function($){ } item.attributes.rl_value = escapeHtml(item.attributes.rl_value); - item.action = '
' + + + if (item.template.toLowerCase() == "default"){ + item.action = '
' + + ' ' + lang.edit + '' + + '
'; + } + else{ + item.action = ''; + } if (Array.isArray(item.attributes.tags)){ var tags = ''; @@ -688,7 +696,7 @@ jQuery(function($){ defaultContent: '' }, { - title: "Template", + title: lang.template, data: 'template', responsivePriority: 3, defaultContent: '' @@ -1115,11 +1123,17 @@ jQuery(function($){ } - - item.action = ''; + if (item.template.toLowerCase() == "default"){ + item.action = '
' + + ' ' + lang.edit + '' + + '
'; + } + else { + item.action = ''; + } if (Array.isArray(item.attributes.tags)){ var tags = ''; @@ -1159,7 +1173,7 @@ jQuery(function($){ defaultContent: '' }, { - title: "Template", + title: lang.template, data: 'template', responsivePriority: 3, defaultContent: '' diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 46e31c90..0a37321f 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -714,6 +714,7 @@ "add_filter": "Filter erstellen", "add_mailbox": "Mailbox hinzufügen", "add_recipient_map_entry": "Empfängerumschreibung hinzufügen", + "add_template": "Vorlage hinzufügen", "add_resource": "Ressource hinzufügen", "add_tls_policy_map": "TLS-Richtlinieneintrag hinzufügen", "address_rewriting": "Adressumschreibung", @@ -755,6 +756,7 @@ "domain": "Domain", "domain_admins": "Domain-Administratoren", "domain_aliases": "Domain-Aliasse", + "domain_templates": "Domainweite Vorlagen", "domain_quota": "Gesamtspeicher", "domain_quota_total": "Domain-Speicherplatz gesamt", "domains": "Domains", @@ -781,6 +783,7 @@ "mailbox_defaults": "Standardeinstellungen", "mailbox_defaults_info": "Steuert die Standardeinstellungen für neue Mailboxen.", "mailbox_defquota": "Standard-Quota", + "mailbox_templates": "Mailboxweite Vorlagen", "mailbox_quota": "Max. Größe einer Mailbox", "mailboxes": "Mailboxen", "max_aliases": "Max. mögliche Aliasse", @@ -810,6 +813,7 @@ "recipient_map_old_info": "Der originale 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", "remove": "Entfernen", "resources": "Ressourcen", "running": "In Ausführung", @@ -836,6 +840,8 @@ "table_size_show_n": "Zeige %s Einträge", "target_address": "Ziel-Adresse", "target_domain": "Ziel-Domain", + "templates": "Vorlagen", + "template": "Vorlage", "tls_enforce_in": "TLS eingehend erzwingen", "tls_enforce_out": "TLS ausgehend erzwingen", "tls_map_dest": "Ziel", @@ -1018,6 +1024,9 @@ "saved_settings": "Regel wurde gespeichert", "settings_map_added": "Regel wurde gespeichert", "settings_map_removed": "Regeln wurden entfernt: %s", + "template_added": "Template %s hinzugefügt", + "template_modified": "Änderungen am Template %s wurden gespeichert", + "template_removed": "Template ID %s wurde gelöscht", "sogo_profile_reset": "ActiveSync-Gerät des Benutzers %s wurde zurückgesetzt", "tls_policy_map_entry_deleted": "TLS-Richtlinie mit der ID %s wurde gelöscht", "tls_policy_map_entry_saved": "TLS-Richtlinieneintrag \"%s\" wurde gespeichert", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index edc696d8..90b208fc 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -1038,6 +1038,7 @@ "sogo_profile_reset": "SOGo profile for user %s was reset", "template_added": "Added template %s", "template_modified": "Changes to template %s have been saved", + "template_removed": "Template ID %s has been deleted", "tls_policy_map_entry_deleted": "TLS policy map ID %s has been deleted", "tls_policy_map_entry_saved": "TLS policy map entry \"%s\" has been saved", "ui_texts": "Saved changes to UI texts", diff --git a/data/web/templates/mailbox.twig b/data/web/templates/mailbox.twig index fa89b001..cdb6a428 100644 --- a/data/web/templates/mailbox.twig +++ b/data/web/templates/mailbox.twig @@ -10,8 +10,6 @@
  • - {# #} - {# #}
    - +
    @@ -59,7 +59,7 @@ {{ lang.debug.show_ip }}
    Loading... -
    + {% else %} {{ lang.admin.ip_check_disabled|raw }} @@ -71,7 +71,7 @@ @@ -198,10 +198,10 @@ - {% endif %} + {% endif %} {% if containers["solr-mailcow"].State.Running == 1 %} -
    +

    Solr Logo

    @@ -238,10 +238,10 @@
    -
    +
    {% endif %} @@ -291,8 +291,8 @@ -
    Hostname
    +
    diff --git a/data/web/templates/edit.twig b/data/web/templates/edit.twig index 29f36435..af83a31d 100644 --- a/data/web/templates/edit.twig +++ b/data/web/templates/edit.twig @@ -26,7 +26,7 @@ var lang_user = {{ lang_user|raw }}; var lang_datatables = {{ lang_datatables|raw }}; var csrf_token = '{{ csrf_token }}'; - var pagination_size = '{{ pagination_size }}'; + var pagination_size = Math.trunc('{{ pagination_size }}'); var table_for_domain = '{{ domain }}'; {% endblock %} diff --git a/data/web/templates/mailbox.twig b/data/web/templates/mailbox.twig index d1044288..1312441c 100644 --- a/data/web/templates/mailbox.twig +++ b/data/web/templates/mailbox.twig @@ -58,7 +58,7 @@ var lang_rl = {{ lang_rl|raw }}; var lang_datatables = {{ lang_datatables|raw }}; var csrf_token = '{{ csrf_token }}'; - var pagination_size = '{{ pagination_size }}'; + var pagination_size = Math.trunc('{{ pagination_size }}'); var role = '{{ role }}'; var is_dual = {{ is_dual }}; var ALLOW_ADMIN_EMAIL_LOGIN = {{ allow_admin_email_login }}; diff --git a/data/web/templates/quarantine.twig b/data/web/templates/quarantine.twig index 5ff7fe66..79b5ea16 100644 --- a/data/web/templates/quarantine.twig +++ b/data/web/templates/quarantine.twig @@ -37,7 +37,7 @@

    {% endif %}

    -
    ID {{ lang.admin.action }}
    +
    {{ lang.quarantine.toggle_all }} @@ -66,7 +66,7 @@ var acl = '{{ acl_json|raw }}'; var lang = {{ lang_quarantine|raw }}; var lang_datatables = {{ lang_datatables|raw }}; var csrf_token = '{{ csrf_token }}'; -var pagination_size = '{{ pagination_size }}'; +var pagination_size = Math.trunc('{{ pagination_size }}'); var role = '{{ role }}'; {% endblock %} diff --git a/data/web/templates/queue.twig b/data/web/templates/queue.twig index 1a5d4ff9..e843c18e 100644 --- a/data/web/templates/queue.twig +++ b/data/web/templates/queue.twig @@ -55,7 +55,7 @@ var lang = {{ lang_queue|raw }}; var lang_datatables = {{ lang_datatables|raw }}; var csrf_token = '{{ csrf_token }}'; - var pagination_size = '{{ pagination_size }}'; + var pagination_size = Math.trunc('{{ pagination_size }}'); var table_for_domain = '{{ domain }}'; {% endblock %} diff --git a/data/web/templates/user_domainadmin_common.twig b/data/web/templates/user_domainadmin_common.twig index 8a7ace39..64a2205d 100644 --- a/data/web/templates/user_domainadmin_common.twig +++ b/data/web/templates/user_domainadmin_common.twig @@ -4,7 +4,7 @@ var acl = '{{ acl_json|raw }}'; var lang = {{ lang_user|raw }}; var csrf_token = '{{ csrf_token }}'; - var pagination_size = '{{ pagination_size }}'; + var pagination_size = Math.trunc('{{ pagination_size }}'); var mailcow_cc_username = '{{ mailcow_cc_username }}'; var user_spam_score = [{{ user_spam_score }}]; var lang_datatables = {{ lang_datatables|raw }}; From 5bf62481d51d0dcd7b8202f65fc73d2e6d349f93 Mon Sep 17 00:00:00 2001 From: Kristian Feldsam Date: Fri, 6 Jan 2023 19:25:23 +0100 Subject: [PATCH 150/170] [Web] Implemented SSO for domain admins Signed-off-by: Kristian Feldsam Revert "[Web] Implemented SSO for domain admins" This reverts commit 6860dc8ebe2c8f53d77df5bca7787f7cb3bb4ee0. Signed-off-by: Kristian Feldsam --- data/web/api/openapi.yaml | 38 +- data/web/inc/functions.domain_admin.inc.php | 875 +++--- data/web/inc/init_db.inc.php | 2731 ++++++++++--------- data/web/inc/sessions.inc.php | 280 +- data/web/inc/triggers.inc.php | 15 +- data/web/json_api.php | 12 + 6 files changed, 2041 insertions(+), 1910 deletions(-) diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index 6310aa58..5e07c4b3 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -699,6 +699,38 @@ paths: type: string type: object summary: Create Domain Admin user + /api/v1/add/sso/domain-admin: + post: + responses: + "401": + $ref: "#/components/responses/Unauthorized" + "200": + content: + application/json: + examples: + response: + value: + token: "591F6D-5C3DD2-7455CD-DAF1C1-AA4FCC" + description: OK + headers: { } + tags: + - Single Sign-On + description: >- + Using this endpoint you can issue a token for Domain Admin user. This token can be used for + autologin Domain Admin user by using query_string var sso_token={token}. Token expiration time is 30s + operationId: Issue Domain Admin SSO token + requestBody: + content: + application/json: + schema: + example: + username: testadmin + properties: + username: + description: the username for the admin user + type: object + type: object + summary: Issue Domain Admin SSO token /api/v1/edit/da-acl: post: responses: @@ -1999,7 +2031,7 @@ paths: - domain.tld - domain2.tld properties: - items: + items: type: array items: type: string @@ -2993,7 +3025,7 @@ paths: application/json: schema: type: array - items: + items: type: object properties: log: @@ -5586,6 +5618,8 @@ tags: description: Manage DKIM keys - name: Domain admin description: Create or udpdate domain admin users + - name: Single Sign-On + description: Issue tokens for users - name: Address Rewriting description: Create BCC maps or recipient maps - name: Outgoing TLS Policy Map Overrides diff --git a/data/web/inc/functions.domain_admin.inc.php b/data/web/inc/functions.domain_admin.inc.php index 804c0f83..bb88ea34 100644 --- a/data/web/inc/functions.domain_admin.inc.php +++ b/data/web/inc/functions.domain_admin.inc.php @@ -1,407 +1,468 @@ - 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - if (empty($domains)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'domain_invalid' - ); - return false; - } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('username_invalid', $username) - ); - return false; - } - - $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - - $stmt = $pdo->prepare("SELECT `username` FROM `admin` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - - $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - - foreach ($num_results as $num_results_each) { - if ($num_results_each != 0) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('object_exists', htmlspecialchars($username)) - ); - return false; - } - } - if (password_check($password, $password2) !== true) { - continue; - } - $password_hashed = hash_password($password); - $valid_domains = 0; - foreach ($domains as $domain) { - if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_invalid', htmlspecialchars($domain)) - ); - continue; - } - $valid_domains++; - $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - VALUES (:username, :domain, :created, :active)"); - $stmt->execute(array( - ':username' => $username, - ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':active' => $active - )); - } - if ($valid_domains != 0) { - $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) - VALUES (:username, :password_hashed, '0', :active)"); - $stmt->execute(array( - ':username' => $username, - ':password_hashed' => $password_hashed, - ':active' => $active - )); - } - $stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)"); - $stmt->execute(array( - ':username' => $username - )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_admin_added', htmlspecialchars($username)) - ); - break; - case 'edit': - if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - // Administrator - if ($_SESSION['mailcow_cc_role'] == "admin") { - if (!is_array($_data['username'])) { - $usernames = array(); - $usernames[] = $_data['username']; - } - else { - $usernames = $_data['username']; - } - foreach ($usernames as $username) { - $is_now = domain_admin('details', $username); - $domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null; - if (!empty($is_now)) { - $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; - $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; - $username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username']; - } - else { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - continue; - } - $password = $_data['password']; - $password2 = $_data['password2']; - if (!empty($domains)) { - foreach ($domains as $domain) { - if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_invalid', htmlspecialchars($domain)) - ); - continue 2; - } - } - } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('username_invalid', $username_new) - ); - continue; - } - if ($username_new != $username) { - if (!empty(domain_admin('details', $username_new)['username'])) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('username_invalid', $username_new) - ); - continue; - } - } - $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array( - ':username_new' => $username_new, - ':username' => $username - )); - if (!empty($domains)) { - foreach ($domains as $domain) { - $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - VALUES (:username_new, :domain, :created, :active)"); - $stmt->execute(array( - ':username_new' => $username_new, - ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':active' => $active - )); - } - } - if (!empty($password)) { - if (password_check($password, $password2) !== true) { - return false; - } - $password_hashed = hash_password($password); - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($_data['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - else { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); - $stmt->execute(array( - ':username_new' => $username_new, - ':username' => $username, - ':active' => $active - )); - if (isset($_data['disable_tfa'])) { - $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - } - else { - $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); - $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); - } - } - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_admin_modified', htmlspecialchars($username)) - ); - } - return true; - } - // Domain administrator - // Can only edit itself - elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { - $username = $_SESSION['mailcow_cc_username']; - $password_old = $_data['user_old_pass']; - $password_new = $_data['user_new_pass']; - $password_new2 = $_data['user_new_pass2']; - - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `username` = :user"); - $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__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - if (password_check($password_new, $password_new2) !== true) { - return false; - } - $password_hashed = hash_password($password_new); - $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); - $stmt->execute(array( - ':password_hashed' => $password_hashed, - ':username' => $username - )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_admin_modified', htmlspecialchars($username)) - ); - } - break; - case 'delete': - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - $usernames = (array)$_data['username']; - foreach ($usernames as $username) { - if (empty(domain_admin('details', $username))) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('username_invalid', $username) - ); - continue; - } - $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => array('domain_admin_removed', htmlspecialchars($username)) - ); - } - break; - case 'get': - $domainadmins = array(); - if ($_SESSION['mailcow_cc_role'] != "admin") { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_action, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - $stmt = $pdo->query("SELECT DISTINCT - `username` - FROM `domain_admins` - WHERE `username` IN ( - SELECT `username` FROM `admin` - WHERE `superadmin`!='1' - )"); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row = array_shift($rows)) { - $domainadmins[] = $row['username']; - } - return $domainadmins; - break; - case 'details': - $domainadmindata = array(); - if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) { - return false; - } - elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { - return false; - } - if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) { - return false; - } - $stmt = $pdo->prepare("SELECT - `tfa`.`active` AS `tfa_active`, - `domain_admins`.`username`, - `domain_admins`.`created`, - `domain_admins`.`active` AS `active` - FROM `domain_admins` - LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` - WHERE `domain_admins`.`username`= :domain_admin"); - $stmt->execute(array( - ':domain_admin' => $_data - )); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - return false; - } - $domainadmindata['username'] = $row['username']; - $domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active']; - $domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active']; - $domainadmindata['active'] = $row['active']; - $domainadmindata['active_int'] = $row['active']; - $domainadmindata['created'] = $row['created']; - // GET SELECTED - $stmt = $pdo->prepare("SELECT `domain` FROM `domain` - WHERE `domain` IN ( - SELECT `domain` FROM `domain_admins` - WHERE `username`= :domain_admin)"); - $stmt->execute(array(':domain_admin' => $_data)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $domainadmindata['selected_domains'][] = $row['domain']; - } - // GET UNSELECTED - $stmt = $pdo->prepare("SELECT `domain` FROM `domain` - WHERE `domain` NOT IN ( - SELECT `domain` FROM `domain_admins` - WHERE `username`= :domain_admin)"); - $stmt->execute(array(':domain_admin' => $_data)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $domainadmindata['unselected_domains'][] = $row['domain']; - } - if (!isset($domainadmindata['unselected_domains'])) { - $domainadmindata['unselected_domains'] = ""; - } - - return $domainadmindata; - break; - } -} + 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + if (empty($domains)) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'domain_invalid' + ); + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username)) || empty ($username) || $username == 'API') { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('username_invalid', $username) + ); + return false; + } + + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + $stmt = $pdo->prepare("SELECT `username` FROM `admin` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results[] = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + foreach ($num_results as $num_results_each) { + if ($num_results_each != 0) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('object_exists', htmlspecialchars($username)) + ); + return false; + } + } + if (password_check($password, $password2) !== true) { + continue; + } + $password_hashed = hash_password($password); + $valid_domains = 0; + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_invalid', htmlspecialchars($domain)) + ); + continue; + } + $valid_domains++; + $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + VALUES (:username, :domain, :created, :active)"); + $stmt->execute(array( + ':username' => $username, + ':domain' => $domain, + ':created' => date('Y-m-d H:i:s'), + ':active' => $active + )); + } + if ($valid_domains != 0) { + $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) + VALUES (:username, :password_hashed, '0', :active)"); + $stmt->execute(array( + ':username' => $username, + ':password_hashed' => $password_hashed, + ':active' => $active + )); + } + $stmt = $pdo->prepare("INSERT INTO `da_acl` (`username`) VALUES (:username)"); + $stmt->execute(array( + ':username' => $username + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_admin_added', htmlspecialchars($username)) + ); + break; + case 'edit': + if ($_SESSION['mailcow_cc_role'] != "admin" && $_SESSION['mailcow_cc_role'] != "domainadmin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + // Administrator + if ($_SESSION['mailcow_cc_role'] == "admin") { + if (!is_array($_data['username'])) { + $usernames = array(); + $usernames[] = $_data['username']; + } + else { + $usernames = $_data['username']; + } + foreach ($usernames as $username) { + $is_now = domain_admin('details', $username); + $domains = (isset($_data['domains'])) ? (array)$_data['domains'] : null; + if (!empty($is_now)) { + $active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active']; + $domains = (!empty($domains)) ? $domains : $is_now['selected_domains']; + $username_new = (!empty($_data['username_new'])) ? $_data['username_new'] : $is_now['username']; + } + else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + continue; + } + $password = $_data['password']; + $password2 = $_data['password2']; + if (!empty($domains)) { + foreach ($domains as $domain) { + if (!is_valid_domain_name($domain) || mailbox('get', 'domain_details', $domain) === false) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_invalid', htmlspecialchars($domain)) + ); + continue 2; + } + } + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username_new))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('username_invalid', $username_new) + ); + continue; + } + if ($username_new != $username) { + if (!empty(domain_admin('details', $username_new)['username'])) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('username_invalid', $username_new) + ); + continue; + } + } + $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("UPDATE `da_acl` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array( + ':username_new' => $username_new, + ':username' => $username + )); + if (!empty($domains)) { + foreach ($domains as $domain) { + $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + VALUES (:username_new, :domain, :created, :active)"); + $stmt->execute(array( + ':username_new' => $username_new, + ':domain' => $domain, + ':created' => date('Y-m-d H:i:s'), + ':active' => $active + )); + } + } + if (!empty($password)) { + if (password_check($password, $password2) !== true) { + return false; + } + $password_hashed = hash_password($password); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active, `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + else { + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username_new, `active` = :active WHERE `username` = :username"); + $stmt->execute(array( + ':username_new' => $username_new, + ':username' => $username, + ':active' => $active + )); + if (isset($_data['disable_tfa'])) { + $stmt = $pdo->prepare("UPDATE `tfa` SET `active` = '0' WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + } + else { + $stmt = $pdo->prepare("UPDATE `tfa` SET `username` = :username_new WHERE `username` = :username"); + $stmt->execute(array(':username_new' => $username_new, ':username' => $username)); + } + } + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_admin_modified', htmlspecialchars($username)) + ); + } + return true; + } + // Domain administrator + // Can only edit itself + elseif ($_SESSION['mailcow_cc_role'] == "domainadmin") { + $username = $_SESSION['mailcow_cc_username']; + $password_old = $_data['user_old_pass']; + $password_new = $_data['user_new_pass']; + $password_new2 = $_data['user_new_pass2']; + + $stmt = $pdo->prepare("SELECT `password` FROM `admin` + WHERE `username` = :user"); + $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__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + if (password_check($password_new, $password_new2) !== true) { + return false; + } + $password_hashed = hash_password($password_new); + $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); + $stmt->execute(array( + ':password_hashed' => $password_hashed, + ':username' => $username + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_admin_modified', htmlspecialchars($username)) + ); + } + break; + case 'delete': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + $usernames = (array)$_data['username']; + foreach ($usernames as $username) { + if (empty(domain_admin('details', $username))) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('username_invalid', $username) + ); + continue; + } + $stmt = $pdo->prepare("DELETE FROM `domain_admins` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `admin` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `da_acl` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $stmt = $pdo->prepare("DELETE FROM `fido2` WHERE `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('domain_admin_removed', htmlspecialchars($username)) + ); + } + break; + case 'get': + $domainadmins = array(); + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } + $stmt = $pdo->query("SELECT DISTINCT + `username` + FROM `domain_admins` + WHERE `username` IN ( + SELECT `username` FROM `admin` + WHERE `superadmin`!='1' + )"); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($rows)) { + $domainadmins[] = $row['username']; + } + return $domainadmins; + break; + case 'details': + $domainadmindata = array(); + if ($_SESSION['mailcow_cc_role'] == "domainadmin" && $_data != $_SESSION['mailcow_cc_username']) { + return false; + } + elseif ($_SESSION['mailcow_cc_role'] != "admin" || !isset($_data)) { + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $_data))) { + return false; + } + $stmt = $pdo->prepare("SELECT + `tfa`.`active` AS `tfa_active`, + `domain_admins`.`username`, + `domain_admins`.`created`, + `domain_admins`.`active` AS `active` + FROM `domain_admins` + LEFT OUTER JOIN `tfa` ON `tfa`.`username`=`domain_admins`.`username` + WHERE `domain_admins`.`username`= :domain_admin"); + $stmt->execute(array( + ':domain_admin' => $_data + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + return false; + } + $domainadmindata['username'] = $row['username']; + $domainadmindata['tfa_active'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active']; + $domainadmindata['tfa_active_int'] = (is_null($row['tfa_active'])) ? 0 : $row['tfa_active']; + $domainadmindata['active'] = $row['active']; + $domainadmindata['active_int'] = $row['active']; + $domainadmindata['created'] = $row['created']; + // GET SELECTED + $stmt = $pdo->prepare("SELECT `domain` FROM `domain` + WHERE `domain` IN ( + SELECT `domain` FROM `domain_admins` + WHERE `username`= :domain_admin)"); + $stmt->execute(array(':domain_admin' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $domainadmindata['selected_domains'][] = $row['domain']; + } + // GET UNSELECTED + $stmt = $pdo->prepare("SELECT `domain` FROM `domain` + WHERE `domain` NOT IN ( + SELECT `domain` FROM `domain_admins` + WHERE `username`= :domain_admin)"); + $stmt->execute(array(':domain_admin' => $_data)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $domainadmindata['unselected_domains'][] = $row['domain']; + } + if (!isset($domainadmindata['unselected_domains'])) { + $domainadmindata['unselected_domains'] = ""; + } + + return $domainadmindata; + break; + } +} +function domain_admin_sso($_action, $_data) { + global $pdo; + + switch ($_action) { + case 'check': + $token = $_data; + + $stmt = $pdo->prepare("SELECT `t1`.`username` FROM `da_sso` AS `t1` JOIN `admin` AS `t2` ON `t1`.`username` = `t2`.`username` WHERE `t1`.`token` = :token AND `t1`.`created` > DATE_SUB(NOW(), INTERVAL '30' SECOND) AND `t2`.`active` = 1 AND `t2`.`superadmin` = 0;"); + $stmt->execute(array( + ':token' => preg_replace('/[^a-zA-Z0-9-]/', '', $token) + )); + $return = $stmt->fetch(PDO::FETCH_ASSOC); + return empty($return['username']) ? false : $return['username']; + case 'issue': + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data), + 'msg' => 'access_denied' + ); + return false; + } + + $username = $_data['username']; + + $stmt = $pdo->prepare("SELECT `username` FROM `domain_admins` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + + if ($num_results < 1) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data), + 'msg' => array('object_doesnt_exist', htmlspecialchars($username)) + ); + return false; + } + + $token = implode('-', array( + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))) + )); + + $stmt = $pdo->prepare("INSERT INTO `da_sso` (`username`, `token`) + VALUES (:username, :token)"); + $stmt->execute(array( + ':username' => $username, + ':token' => $token + )); + + // perform cleanup + $pdo->query("DELETE FROM `da_sso` WHERE created < DATE_SUB(NOW(), INTERVAL '30' SECOND);"); + + return ['token' => $token]; + break; + } +} diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index e781f944..9fc9234e 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -1,230 +1,230 @@ -query("SHOW TABLES LIKE 'versions'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'db_schema'"); - if ($stmt->fetch(PDO::FETCH_ASSOC)['version'] == $db_version) { - return true; - } - if (!preg_match('/y|yes/i', getenv('MASTER'))) { - $_SESSION['return'][] = array( - 'type' => 'warning', - 'log' => array(__FUNCTION__), - 'msg' => 'Database not initialized: not running db_init on slave.' - ); - return true; - } - } - - $views = array( - "grouped_mail_aliases" => "CREATE VIEW grouped_mail_aliases (username, aliases) AS - SELECT goto, IFNULL(GROUP_CONCAT(address ORDER BY address SEPARATOR ' '), '') AS address FROM alias - WHERE address!=goto - AND active = '1' - AND sogo_visible = '1' - AND address NOT LIKE '@%' - GROUP BY goto;", - // START - // Unused at the moment - we cannot allow to show a foreign mailbox as sender address in SOGo, as SOGo does not like this - // We need to create delegation in SOGo AND set a sender_acl in mailcow to allow to send as user X - "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as_acl) AS - SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl - WHERE send_as NOT LIKE '@%' - GROUP BY logged_in_as;", - // END - "grouped_sender_acl_external" => "CREATE VIEW grouped_sender_acl_external (username, send_as_acl) AS - SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl - WHERE send_as NOT LIKE '@%' AND external = '1' - GROUP BY logged_in_as;", - "grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS - SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox - LEFT OUTER JOIN alias_domain ON target_domain=domain - GROUP BY username;", - "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS - SELECT md5(script_data), username, script_name, script_data FROM sieve_filters - WHERE filter_type = 'prefilter';", - "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS - SELECT md5(script_data), username, script_name, script_data FROM sieve_filters - WHERE filter_type = 'postfilter';" - ); - - $tables = array( - "versions" => array( - "cols" => array( - "application" => "VARCHAR(255) NOT NULL", - "version" => "VARCHAR(100) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - ), - "keys" => array( - "primary" => array( - "" => array("application") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "admin" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "password" => "VARCHAR(255) NOT NULL", - "superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE NOW(0)", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "fido2" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "friendlyName" => "VARCHAR(255)", - "rpId" => "VARCHAR(255) NOT NULL", - "credentialPublicKey" => "TEXT NOT NULL", - "certificateChain" => "TEXT", - // Can be null for format "none" - "certificate" => "TEXT", - "certificateIssuer" => "VARCHAR(255)", - "certificateSubject" => "VARCHAR(255)", - "signatureCounter" => "INT", - "AAGUID" => "BLOB", - "credentialId" => "BLOB NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE NOW(0)", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "_sogo_static_view" => array( - "cols" => array( - "c_uid" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_password" => "VARCHAR(255) NOT NULL DEFAULT ''", - "c_cn" => "VARCHAR(255)", - "mail" => "VARCHAR(255) NOT NULL", - // TODO -> use TEXT and check if SOGo login breaks on empty aliases - "aliases" => "TEXT NOT NULL", - "ad_aliases" => "VARCHAR(6144) NOT NULL DEFAULT ''", - "ext_acl" => "VARCHAR(6144) NOT NULL DEFAULT ''", - "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", - "multiple_bookings" => "INT NOT NULL DEFAULT -1" - ), - "keys" => array( - "primary" => array( - "" => array("c_uid") - ), - "key" => array( - "domain" => array("domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "relayhosts" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "hostname" => "VARCHAR(255) NOT NULL", - "username" => "VARCHAR(255) NOT NULL", - "password" => "VARCHAR(255) NOT NULL", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "hostname" => array("hostname") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "transports" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "destination" => "VARCHAR(255) NOT NULL", - "nexthop" => "VARCHAR(255) NOT NULL", - "username" => "VARCHAR(255) NOT NULL DEFAULT ''", - "password" => "VARCHAR(255) NOT NULL DEFAULT ''", - "is_mx_based" => "TINYINT(1) NOT NULL DEFAULT '0'", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "destination" => array("destination"), - "nexthop" => array("nexthop"), - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "alias" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "address" => "VARCHAR(255) NOT NULL", - "goto" => "TEXT NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "private_comment" => "TEXT", - "public_comment" => "TEXT", - "sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "unique" => array( - "address" => array("address") - ), - "key" => array( - "domain" => array("domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "api" => array( - "cols" => array( - "api_key" => "VARCHAR(255) NOT NULL", - "allow_from" => "VARCHAR(512) NOT NULL", - "skip_ip_check" => "TINYINT(1) NOT NULL DEFAULT '0'", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE NOW(0)", - "access" => "ENUM('ro', 'rw') NOT NULL DEFAULT 'rw'", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("api_key") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sender_acl" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "logged_in_as" => "VARCHAR(255) NOT NULL", - "send_as" => "VARCHAR(255) NOT NULL", - "external" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), +query("SHOW TABLES LIKE 'versions'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->query("SELECT `version` FROM `versions` WHERE `application` = 'db_schema'"); + if ($stmt->fetch(PDO::FETCH_ASSOC)['version'] == $db_version) { + return true; + } + if (!preg_match('/y|yes/i', getenv('MASTER'))) { + $_SESSION['return'][] = array( + 'type' => 'warning', + 'log' => array(__FUNCTION__), + 'msg' => 'Database not initialized: not running db_init on slave.' + ); + return true; + } + } + + $views = array( + "grouped_mail_aliases" => "CREATE VIEW grouped_mail_aliases (username, aliases) AS + SELECT goto, IFNULL(GROUP_CONCAT(address ORDER BY address SEPARATOR ' '), '') AS address FROM alias + WHERE address!=goto + AND active = '1' + AND sogo_visible = '1' + AND address NOT LIKE '@%' + GROUP BY goto;", + // START + // Unused at the moment - we cannot allow to show a foreign mailbox as sender address in SOGo, as SOGo does not like this + // We need to create delegation in SOGo AND set a sender_acl in mailcow to allow to send as user X + "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as_acl) AS + SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl + WHERE send_as NOT LIKE '@%' + GROUP BY logged_in_as;", + // END + "grouped_sender_acl_external" => "CREATE VIEW grouped_sender_acl_external (username, send_as_acl) AS + SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as_acl FROM sender_acl + WHERE send_as NOT LIKE '@%' AND external = '1' + GROUP BY logged_in_as;", + "grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS + SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox + LEFT OUTER JOIN alias_domain ON target_domain=domain + GROUP BY username;", + "sieve_before" => "CREATE VIEW sieve_before (id, username, script_name, script_data) AS + SELECT md5(script_data), username, script_name, script_data FROM sieve_filters + WHERE filter_type = 'prefilter';", + "sieve_after" => "CREATE VIEW sieve_after (id, username, script_name, script_data) AS + SELECT md5(script_data), username, script_name, script_data FROM sieve_filters + WHERE filter_type = 'postfilter';" + ); + + $tables = array( + "versions" => array( + "cols" => array( + "application" => "VARCHAR(255) NOT NULL", + "version" => "VARCHAR(100) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + ), + "keys" => array( + "primary" => array( + "" => array("application") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "admin" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE NOW(0)", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "fido2" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "friendlyName" => "VARCHAR(255)", + "rpId" => "VARCHAR(255) NOT NULL", + "credentialPublicKey" => "TEXT NOT NULL", + "certificateChain" => "TEXT", + // Can be null for format "none" + "certificate" => "TEXT", + "certificateIssuer" => "VARCHAR(255)", + "certificateSubject" => "VARCHAR(255)", + "signatureCounter" => "INT", + "AAGUID" => "BLOB", + "credentialId" => "BLOB NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE NOW(0)", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "_sogo_static_view" => array( + "cols" => array( + "c_uid" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_password" => "VARCHAR(255) NOT NULL DEFAULT ''", + "c_cn" => "VARCHAR(255)", + "mail" => "VARCHAR(255) NOT NULL", + // TODO -> use TEXT and check if SOGo login breaks on empty aliases + "aliases" => "TEXT NOT NULL", + "ad_aliases" => "VARCHAR(6144) NOT NULL DEFAULT ''", + "ext_acl" => "VARCHAR(6144) NOT NULL DEFAULT ''", + "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", + "multiple_bookings" => "INT NOT NULL DEFAULT -1" + ), + "keys" => array( + "primary" => array( + "" => array("c_uid") + ), + "key" => array( + "domain" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "relayhosts" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "hostname" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "hostname" => array("hostname") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "transports" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "destination" => "VARCHAR(255) NOT NULL", + "nexthop" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL DEFAULT ''", + "password" => "VARCHAR(255) NOT NULL DEFAULT ''", + "is_mx_based" => "TINYINT(1) NOT NULL DEFAULT '0'", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "destination" => array("destination"), + "nexthop" => array("nexthop"), + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "alias" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "address" => "VARCHAR(255) NOT NULL", + "goto" => "TEXT NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "private_comment" => "TEXT", + "public_comment" => "TEXT", + "sogo_visible" => "TINYINT(1) NOT NULL DEFAULT '1'", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "unique" => array( + "address" => array("address") + ), + "key" => array( + "domain" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "api" => array( + "cols" => array( + "api_key" => "VARCHAR(255) NOT NULL", + "allow_from" => "VARCHAR(512) NOT NULL", + "skip_ip_check" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE NOW(0)", + "access" => "ENUM('ro', 'rw') NOT NULL DEFAULT 'rw'", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("api_key") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sender_acl" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "logged_in_as" => "VARCHAR(255) NOT NULL", + "send_as" => "VARCHAR(255) NOT NULL", + "external" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), "templates" => array( "cols" => array( "id" => "INT NOT NULL AUTO_INCREMENT", @@ -241,1074 +241,1087 @@ function init_db_schema() { ), "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" ), - "domain" => array( - // Todo: Move some attributes to json - "cols" => array( - "domain" => "VARCHAR(255) NOT NULL", - "description" => "VARCHAR(255)", - "aliases" => "INT(10) NOT NULL DEFAULT '0'", - "mailboxes" => "INT(10) NOT NULL DEFAULT '0'", - "defquota" => "BIGINT(20) NOT NULL DEFAULT '3072'", - "maxquota" => "BIGINT(20) NOT NULL DEFAULT '102400'", - "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", - "relayhost" => "VARCHAR(255) NOT NULL DEFAULT '0'", - "backupmx" => "TINYINT(1) NOT NULL DEFAULT '0'", - "gal" => "TINYINT(1) NOT NULL DEFAULT '1'", - "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'", - "relay_unknown_only" => "TINYINT(1) NOT NULL DEFAULT '0'", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "tags_domain" => array( - "cols" => array( - "tag_name" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL" - ), - "keys" => array( - "fkey" => array( - "fk_tags_domain" => array( - "col" => "domain", - "ref" => "domain.domain", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ), - "unique" => array( - "tag_name" => array("tag_name", "domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "tls_policy_override" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "dest" => "VARCHAR(255) NOT NULL", - "policy" => "ENUM('none', 'may', 'encrypt', 'dane', 'dane-only', 'fingerprint', 'verify', 'secure') NOT NULL", - "parameters" => "VARCHAR(255) DEFAULT ''", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "unique" => array( - "dest" => array("dest") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "quarantine" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "qid" => "VARCHAR(30) NOT NULL", - "subject" => "VARCHAR(500)", - "score" => "FLOAT(8,2)", - "ip" => "VARCHAR(50)", - "action" => "CHAR(20) NOT NULL DEFAULT 'unknown'", - "symbols" => "JSON", - "fuzzy_hashes" => "JSON", - "sender" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", - "rcpt" => "VARCHAR(255)", - "msg" => "LONGTEXT", - "domain" => "VARCHAR(255)", - "notified" => "TINYINT(1) NOT NULL DEFAULT '0'", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "mailbox" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "password" => "VARCHAR(255) NOT NULL", - "name" => "VARCHAR(255)", - "description" => "VARCHAR(255)", - // mailbox_path_prefix is followed by domain/local_part/ - "mailbox_path_prefix" => "VARCHAR(150) DEFAULT '/var/vmail/'", - "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", - "local_part" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "attributes" => "JSON", - "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", - "multiple_bookings" => "INT NOT NULL DEFAULT -1", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ), - "key" => array( - "domain" => array("domain"), - "kind" => array("kind") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "tags_mailbox" => array( - "cols" => array( - "tag_name" => "VARCHAR(255) NOT NULL", - "username" => "VARCHAR(255) NOT NULL" - ), - "keys" => array( - "fkey" => array( - "fk_tags_mailbox" => array( - "col" => "username", - "ref" => "mailbox.username", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ), - "unique" => array( - "tag_name" => array("tag_name", "username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sieve_filters" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "username" => "VARCHAR(255) NOT NULL", - "script_desc" => "VARCHAR(255) NOT NULL", - "script_name" => "ENUM('active','inactive')", - "script_data" => "TEXT NOT NULL", - "filter_type" => "ENUM('postfilter','prefilter')", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "username" => array("username"), - "script_desc" => array("script_desc") - ), - "fkey" => array( - "fk_username_sieve_global_before" => array( - "col" => "username", - "ref" => "mailbox.username", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "app_passwd" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "name" => "VARCHAR(255) NOT NULL", - "mailbox" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "password" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "imap_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "smtp_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "dav_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "eas_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "pop3_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "sieve_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "mailbox" => array("mailbox"), - "password" => array("password"), - "domain" => array("domain"), - ), - "fkey" => array( - "fk_username_app_passwd" => array( - "col" => "mailbox", - "ref" => "mailbox.username", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "user_acl" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "spam_alias" => "TINYINT(1) NOT NULL DEFAULT '1'", - "tls_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", - "spam_score" => "TINYINT(1) NOT NULL DEFAULT '1'", - "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", - "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'", - "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '0'", - "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'", - "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'", - "pushover" => "TINYINT(1) NOT NULL DEFAULT '1'", - // quarantine is for quarantine actions, todo: rename - "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", - "quarantine_attachments" => "TINYINT(1) NOT NULL DEFAULT '1'", - "quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'", - "quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'", - "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", - ), - "keys" => array( - "primary" => array( - "" => array("username") - ), - "fkey" => array( - "fk_username" => array( - "col" => "username", - "ref" => "mailbox.username", - "delete" => "CASCADE", - "update" => "NO ACTION" - ) - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "alias_domain" => array( - "cols" => array( - "alias_domain" => "VARCHAR(255) NOT NULL", - "target_domain" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("alias_domain") - ), - "key" => array( - "active" => array("active"), - "target_domain" => array("target_domain") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "spamalias" => array( - "cols" => array( - "address" => "VARCHAR(255) NOT NULL", - "goto" => "TEXT NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "validity" => "INT(11)" - ), - "keys" => array( - "primary" => array( - "" => array("address") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "filterconf" => array( - "cols" => array( - "object" => "VARCHAR(255) NOT NULL DEFAULT ''", - "option" => "VARCHAR(50) NOT NULL DEFAULT ''", - "value" => "VARCHAR(100) NOT NULL DEFAULT ''", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "prefid" => "INT(11) NOT NULL AUTO_INCREMENT" - ), - "keys" => array( - "primary" => array( - "" => array("prefid") - ), - "key" => array( - "object" => array("object") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "settingsmap" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "desc" => "VARCHAR(255) NOT NULL", - "content" => "LONGTEXT NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "logs" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "task" => "CHAR(32) NOT NULL DEFAULT '000000'", - "type" => "VARCHAR(32) DEFAULT ''", - "msg" => "TEXT", - "call" => "TEXT", - "user" => "VARCHAR(64) NOT NULL", - "role" => "VARCHAR(32) NOT NULL", - "remote" => "VARCHAR(39) NOT NULL", - "time" => "INT(11) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sasl_log" => array( - "cols" => array( - "service" => "VARCHAR(32) NOT NULL DEFAULT ''", - "app_password" => "INT", - "username" => "VARCHAR(255) NOT NULL", - "real_rip" => "VARCHAR(64) NOT NULL", - "datetime" => "DATETIME(0) NOT NULL DEFAULT NOW(0)" - ), - "keys" => array( - "primary" => array( - "" => array("service", "real_rip", "username") - ), - "key" => array( - "username" => array("username"), - "service" => array("service"), - "datetime" => array("datetime"), - "real_rip" => array("real_rip") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "quota2" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "bytes" => "BIGINT(20) NOT NULL DEFAULT '0'", - "messages" => "BIGINT(20) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "quota2replica" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "bytes" => "BIGINT(20) NOT NULL DEFAULT '0'", - "messages" => "BIGINT(20) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "domain_admins" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "username" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "username" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "da_acl" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'", - "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", - "login_as" => "TINYINT(1) NOT NULL DEFAULT '1'", - "sogo_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", - "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'", - "pushover" => "TINYINT(1) NOT NULL DEFAULT '0'", - "filters" => "TINYINT(1) NOT NULL DEFAULT '1'", - "ratelimit" => "TINYINT(1) NOT NULL DEFAULT '1'", - "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", - "extend_sender_acl" => "TINYINT(1) NOT NULL DEFAULT '0'", - "unlimited_quota" => "TINYINT(1) NOT NULL DEFAULT '0'", - "protocol_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "smtp_ip_access" => "TINYINT(1) NOT NULL DEFAULT '1'", - "alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'", - "mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", - "domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", - "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "imapsync" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "user2" => "VARCHAR(255) NOT NULL", - "host1" => "VARCHAR(255) NOT NULL", - "authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'", - "regextrans2" => "VARCHAR(255) DEFAULT ''", - "authmd51" => "TINYINT(1) NOT NULL DEFAULT 0", - "domain2" => "VARCHAR(255) NOT NULL DEFAULT ''", - "subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''", - "user1" => "VARCHAR(255) NOT NULL", - "password1" => "VARCHAR(255) NOT NULL", - "exclude" => "VARCHAR(500) NOT NULL DEFAULT ''", - "maxage" => "SMALLINT NOT NULL DEFAULT '0'", - "mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'", - "maxbytespersecond" => "VARCHAR(50) NOT NULL DEFAULT '0'", - "port1" => "SMALLINT UNSIGNED NOT NULL", - "enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'", - "delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'", - "delete1" => "TINYINT(1) NOT NULL DEFAULT '0'", - "delete2" => "TINYINT(1) NOT NULL DEFAULT '0'", - "automap" => "TINYINT(1) NOT NULL DEFAULT '0'", - "skipcrossduplicates" => "TINYINT(1) NOT NULL DEFAULT '0'", - "custom_params" => "VARCHAR(512) NOT NULL DEFAULT ''", - "timeout1" => "SMALLINT NOT NULL DEFAULT '600'", - "timeout2" => "SMALLINT NOT NULL DEFAULT '600'", - "subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'", - "is_running" => "TINYINT(1) NOT NULL DEFAULT '0'", - "returned_text" => "LONGTEXT", - "last_run" => "TIMESTAMP NULL DEFAULT NULL", - "success" => "TINYINT(1) UNSIGNED DEFAULT NULL", - "exit_status" => "VARCHAR(50) DEFAULT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "bcc_maps" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "local_dest" => "VARCHAR(255) NOT NULL", - "bcc_dest" => "VARCHAR(255) NOT NULL", - "domain" => "VARCHAR(255) NOT NULL", - "type" => "ENUM('sender','rcpt')", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "local_dest" => array("local_dest"), - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "recipient_maps" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "old_dest" => "VARCHAR(255) NOT NULL", - "new_dest" => "VARCHAR(255) NOT NULL", - "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", - "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "local_dest" => array("old_dest"), - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "tfa" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "key_id" => "VARCHAR(255) NOT NULL", - "username" => "VARCHAR(255) NOT NULL", - "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')", - "secret" => "VARCHAR(255) DEFAULT NULL", - "keyHandle" => "VARCHAR(1023) DEFAULT NULL", - "publicKey" => "VARCHAR(4096) DEFAULT NULL", - "counter" => "INT NOT NULL DEFAULT '0'", - "certificate" => "TEXT", - "active" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "forwarding_hosts" => array( - "cols" => array( - "host" => "VARCHAR(255) NOT NULL", - "source" => "VARCHAR(255) NOT NULL", - "filter_spam" => "TINYINT(1) NOT NULL DEFAULT '0'" - ), - "keys" => array( - "primary" => array( - "" => array("host") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_acl" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "c_folder_id" => "INT NOT NULL", - "c_object" => "VARCHAR(255) NOT NULL", - "c_uid" => "VARCHAR(255) NOT NULL", - "c_role" => "VARCHAR(80) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ), - "key" => array( - "sogo_acl_c_folder_id_idx" => array("c_folder_id"), - "sogo_acl_c_uid_idx" => array("c_uid") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_alarms_folder" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "c_path" => "VARCHAR(255) NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_uid" => "VARCHAR(255) NOT NULL", - "c_recurrence_id" => "INT(11) DEFAULT NULL", - "c_alarm_number" => "INT(11) NOT NULL", - "c_alarm_date" => "INT(11) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_cache_folder" => array( - "cols" => array( - "c_uid" => "VARCHAR(255) NOT NULL", - "c_path" => "VARCHAR(255) NOT NULL", - "c_parent_path" => "VARCHAR(255) DEFAULT NULL", - "c_type" => "TINYINT(3) unsigned NOT NULL", - "c_creationdate" => "INT(11) NOT NULL", - "c_lastmodified" => "INT(11) NOT NULL", - "c_version" => "INT(11) NOT NULL DEFAULT '0'", - "c_deleted" => "TINYINT(4) NOT NULL DEFAULT '0'", - "c_content" => "LONGTEXT" - ), - "keys" => array( - "primary" => array( - "" => array("c_uid", "c_path") - ), - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_folder_info" => array( - "cols" => array( - "c_folder_id" => "BIGINT(20) unsigned NOT NULL AUTO_INCREMENT", - "c_path" => "VARCHAR(255) NOT NULL", - "c_path1" => "VARCHAR(255) NOT NULL", - "c_path2" => "VARCHAR(255) DEFAULT NULL", - "c_path3" => "VARCHAR(255) DEFAULT NULL", - "c_path4" => "VARCHAR(255) DEFAULT NULL", - "c_foldername" => "VARCHAR(255) NOT NULL", - "c_location" => "VARCHAR(2048) DEFAULT NULL", - "c_quick_location" => "VARCHAR(2048) DEFAULT NULL", - "c_acl_location" => "VARCHAR(2048) DEFAULT NULL", - "c_folder_type" => "VARCHAR(255) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("c_path") - ), - "unique" => array( - "c_folder_id" => array("c_folder_id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_quick_appointment" => array( - "cols" => array( - "c_folder_id" => "INT NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_uid" => "VARCHAR(1000) NOT NULL", - "c_startdate" => "INT", - "c_enddate" => "INT", - "c_cycleenddate" => "INT", - "c_title" => "VARCHAR(1000) NOT NULL", - "c_participants" => "TEXT", - "c_isallday" => "INT", - "c_iscycle" => "INT", - "c_cycleinfo" => "TEXT", - "c_classification" => "INT NOT NULL", - "c_isopaque" => "INT NOT NULL", - "c_status" => "INT NOT NULL", - "c_priority" => "INT", - "c_location" => "VARCHAR(255)", - "c_orgmail" => "VARCHAR(255)", - "c_partmails" => "TEXT", - "c_partstates" => "TEXT", - "c_category" => "VARCHAR(255)", - "c_sequence" => "INT", - "c_component" => "VARCHAR(10) NOT NULL", - "c_nextalarm" => "INT", - "c_description" => "TEXT" - ), - "keys" => array( - "primary" => array( - "" => array("c_folder_id", "c_name") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_quick_contact" => array( - "cols" => array( - "c_folder_id" => "INT NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_givenname" => "VARCHAR(255)", - "c_cn" => "VARCHAR(255)", - "c_sn" => "VARCHAR(255)", - "c_screenname" => "VARCHAR(255)", - "c_l" => "VARCHAR(255)", - "c_mail" => "TEXT", - "c_o" => "VARCHAR(500)", - "c_ou" => "VARCHAR(255)", - "c_telephonenumber" => "VARCHAR(255)", - "c_categories" => "VARCHAR(255)", - "c_component" => "VARCHAR(10) NOT NULL", - "c_hascertificate" => "INT4 DEFAULT 0" - ), - "keys" => array( - "primary" => array( - "" => array("c_folder_id", "c_name") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_sessions_folder" => array( - "cols" => array( - "c_id" => "VARCHAR(255) NOT NULL", - "c_value" => "VARCHAR(4096) NOT NULL", - "c_creationdate" => "INT(11) NOT NULL", - "c_lastseen" => "INT(11) NOT NULL" - ), - "keys" => array( - "primary" => array( - "" => array("c_id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_store" => array( - "cols" => array( - "c_folder_id" => "INT NOT NULL", - "c_name" => "VARCHAR(255) NOT NULL", - "c_content" => "MEDIUMTEXT NOT NULL", - "c_creationdate" => "INT NOT NULL", - "c_lastmodified" => "INT NOT NULL", - "c_version" => "INT NOT NULL", - "c_deleted" => "INT" - ), - "keys" => array( - "primary" => array( - "" => array("c_folder_id", "c_name") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "pushover" => array( - "cols" => array( - "username" => "VARCHAR(255) NOT NULL", - "key" => "VARCHAR(255) NOT NULL", - "token" => "VARCHAR(255) NOT NULL", - "attributes" => "JSON", - "title" => "TEXT", - "text" => "TEXT", - "senders" => "TEXT", - "senders_regex" => "TEXT", - "active" => "TINYINT(1) NOT NULL DEFAULT '1'" - ), - "keys" => array( - "primary" => array( - "" => array("username") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "sogo_user_profile" => array( - "cols" => array( - "c_uid" => "VARCHAR(255) NOT NULL", - "c_defaults" => "LONGTEXT", - "c_settings" => "LONGTEXT" - ), - "keys" => array( - "primary" => array( - "" => array("c_uid") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "oauth_clients" => array( - "cols" => array( - "id" => "INT NOT NULL AUTO_INCREMENT", - "client_id" => "VARCHAR(80) NOT NULL", - "client_secret" => "VARCHAR(80)", - "redirect_uri" => "VARCHAR(2000)", - "grant_types" => "VARCHAR(80)", - "scope" => "VARCHAR(4000)", - "user_id" => "VARCHAR(80)" - ), - "keys" => array( - "primary" => array( - "" => array("client_id") - ), - "unique" => array( - "id" => array("id") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "oauth_access_tokens" => array( - "cols" => array( - "access_token" => "VARCHAR(40) NOT NULL", - "client_id" => "VARCHAR(80) NOT NULL", - "user_id" => "VARCHAR(80)", - "expires" => "TIMESTAMP NOT NULL", - "scope" => "VARCHAR(4000)" - ), - "keys" => array( - "primary" => array( - "" => array("access_token") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "oauth_authorization_codes" => array( - "cols" => array( - "authorization_code" => "VARCHAR(40) NOT NULL", - "client_id" => "VARCHAR(80) NOT NULL", - "user_id" => "VARCHAR(80)", - "redirect_uri" => "VARCHAR(2000)", - "expires" => "TIMESTAMP NOT NULL", - "scope" => "VARCHAR(4000)", - "id_token" => "VARCHAR(1000)" - ), - "keys" => array( - "primary" => array( - "" => array("authorization_code") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ), - "oauth_refresh_tokens" => array( - "cols" => array( - "refresh_token" => "VARCHAR(40) NOT NULL", - "client_id" => "VARCHAR(80) NOT NULL", - "user_id" => "VARCHAR(80)", - "expires" => "TIMESTAMP NOT NULL", - "scope" => "VARCHAR(4000)" - ), - "keys" => array( - "primary" => array( - "" => array("refresh_token") - ) - ), - "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" - ) - ); - - foreach ($tables as $table => $properties) { - // Migrate to quarantine - if ($table == 'quarantine') { - $stmt = $pdo->query("SHOW TABLES LIKE 'quarantaine'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->query("SHOW TABLES LIKE 'quarantine'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("RENAME TABLE `quarantaine` TO `quarantine`"); - } - } - } - + "domain" => array( + // Todo: Move some attributes to json + "cols" => array( + "domain" => "VARCHAR(255) NOT NULL", + "description" => "VARCHAR(255)", + "aliases" => "INT(10) NOT NULL DEFAULT '0'", + "mailboxes" => "INT(10) NOT NULL DEFAULT '0'", + "defquota" => "BIGINT(20) NOT NULL DEFAULT '3072'", + "maxquota" => "BIGINT(20) NOT NULL DEFAULT '102400'", + "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", + "relayhost" => "VARCHAR(255) NOT NULL DEFAULT '0'", + "backupmx" => "TINYINT(1) NOT NULL DEFAULT '0'", + "gal" => "TINYINT(1) NOT NULL DEFAULT '1'", + "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'", + "relay_unknown_only" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "tags_domain" => array( + "cols" => array( + "tag_name" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "fkey" => array( + "fk_tags_domain" => array( + "col" => "domain", + "ref" => "domain.domain", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ), + "unique" => array( + "tag_name" => array("tag_name", "domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "tls_policy_override" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "dest" => "VARCHAR(255) NOT NULL", + "policy" => "ENUM('none', 'may', 'encrypt', 'dane', 'dane-only', 'fingerprint', 'verify', 'secure') NOT NULL", + "parameters" => "VARCHAR(255) DEFAULT ''", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "unique" => array( + "dest" => array("dest") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "quarantine" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "qid" => "VARCHAR(30) NOT NULL", + "subject" => "VARCHAR(500)", + "score" => "FLOAT(8,2)", + "ip" => "VARCHAR(50)", + "action" => "CHAR(20) NOT NULL DEFAULT 'unknown'", + "symbols" => "JSON", + "fuzzy_hashes" => "JSON", + "sender" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", + "rcpt" => "VARCHAR(255)", + "msg" => "LONGTEXT", + "domain" => "VARCHAR(255)", + "notified" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "user" => "VARCHAR(255) NOT NULL DEFAULT 'unknown'", + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "mailbox" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "name" => "VARCHAR(255)", + "description" => "VARCHAR(255)", + // mailbox_path_prefix is followed by domain/local_part/ + "mailbox_path_prefix" => "VARCHAR(150) DEFAULT '/var/vmail/'", + "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", + "local_part" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "attributes" => "JSON", + "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", + "multiple_bookings" => "INT NOT NULL DEFAULT -1", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ), + "key" => array( + "domain" => array("domain"), + "kind" => array("kind") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "tags_mailbox" => array( + "cols" => array( + "tag_name" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "fkey" => array( + "fk_tags_mailbox" => array( + "col" => "username", + "ref" => "mailbox.username", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ), + "unique" => array( + "tag_name" => array("tag_name", "username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sieve_filters" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "username" => "VARCHAR(255) NOT NULL", + "script_desc" => "VARCHAR(255) NOT NULL", + "script_name" => "ENUM('active','inactive')", + "script_data" => "TEXT NOT NULL", + "filter_type" => "ENUM('postfilter','prefilter')", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "username" => array("username"), + "script_desc" => array("script_desc") + ), + "fkey" => array( + "fk_username_sieve_global_before" => array( + "col" => "username", + "ref" => "mailbox.username", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "app_passwd" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "name" => "VARCHAR(255) NOT NULL", + "mailbox" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "imap_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "smtp_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "dav_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "eas_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "pop3_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "sieve_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "mailbox" => array("mailbox"), + "password" => array("password"), + "domain" => array("domain"), + ), + "fkey" => array( + "fk_username_app_passwd" => array( + "col" => "mailbox", + "ref" => "mailbox.username", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "user_acl" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "spam_alias" => "TINYINT(1) NOT NULL DEFAULT '1'", + "tls_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", + "spam_score" => "TINYINT(1) NOT NULL DEFAULT '1'", + "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", + "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'", + "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '0'", + "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'", + "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'", + "pushover" => "TINYINT(1) NOT NULL DEFAULT '1'", + // quarantine is for quarantine actions, todo: rename + "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine_attachments" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine_notification" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine_category" => "TINYINT(1) NOT NULL DEFAULT '1'", + "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", + ), + "keys" => array( + "primary" => array( + "" => array("username") + ), + "fkey" => array( + "fk_username" => array( + "col" => "username", + "ref" => "mailbox.username", + "delete" => "CASCADE", + "update" => "NO ACTION" + ) + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "alias_domain" => array( + "cols" => array( + "alias_domain" => "VARCHAR(255) NOT NULL", + "target_domain" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("alias_domain") + ), + "key" => array( + "active" => array("active"), + "target_domain" => array("target_domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "spamalias" => array( + "cols" => array( + "address" => "VARCHAR(255) NOT NULL", + "goto" => "TEXT NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "validity" => "INT(11)" + ), + "keys" => array( + "primary" => array( + "" => array("address") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "filterconf" => array( + "cols" => array( + "object" => "VARCHAR(255) NOT NULL DEFAULT ''", + "option" => "VARCHAR(50) NOT NULL DEFAULT ''", + "value" => "VARCHAR(100) NOT NULL DEFAULT ''", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "prefid" => "INT(11) NOT NULL AUTO_INCREMENT" + ), + "keys" => array( + "primary" => array( + "" => array("prefid") + ), + "key" => array( + "object" => array("object") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "settingsmap" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "desc" => "VARCHAR(255) NOT NULL", + "content" => "LONGTEXT NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "logs" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "task" => "CHAR(32) NOT NULL DEFAULT '000000'", + "type" => "VARCHAR(32) DEFAULT ''", + "msg" => "TEXT", + "call" => "TEXT", + "user" => "VARCHAR(64) NOT NULL", + "role" => "VARCHAR(32) NOT NULL", + "remote" => "VARCHAR(39) NOT NULL", + "time" => "INT(11) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sasl_log" => array( + "cols" => array( + "service" => "VARCHAR(32) NOT NULL DEFAULT ''", + "app_password" => "INT", + "username" => "VARCHAR(255) NOT NULL", + "real_rip" => "VARCHAR(64) NOT NULL", + "datetime" => "DATETIME(0) NOT NULL DEFAULT NOW(0)" + ), + "keys" => array( + "primary" => array( + "" => array("service", "real_rip", "username") + ), + "key" => array( + "username" => array("username"), + "service" => array("service"), + "datetime" => array("datetime"), + "real_rip" => array("real_rip") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "quota2" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "bytes" => "BIGINT(20) NOT NULL DEFAULT '0'", + "messages" => "BIGINT(20) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "quota2replica" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "bytes" => "BIGINT(20) NOT NULL DEFAULT '0'", + "messages" => "BIGINT(20) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "domain_admins" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "username" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "username" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "da_acl" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'", + "quarantine" => "TINYINT(1) NOT NULL DEFAULT '1'", + "login_as" => "TINYINT(1) NOT NULL DEFAULT '1'", + "sogo_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "app_passwds" => "TINYINT(1) NOT NULL DEFAULT '1'", + "bcc_maps" => "TINYINT(1) NOT NULL DEFAULT '1'", + "pushover" => "TINYINT(1) NOT NULL DEFAULT '0'", + "filters" => "TINYINT(1) NOT NULL DEFAULT '1'", + "ratelimit" => "TINYINT(1) NOT NULL DEFAULT '1'", + "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", + "extend_sender_acl" => "TINYINT(1) NOT NULL DEFAULT '0'", + "unlimited_quota" => "TINYINT(1) NOT NULL DEFAULT '0'", + "protocol_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "smtp_ip_access" => "TINYINT(1) NOT NULL DEFAULT '1'", + "alias_domains" => "TINYINT(1) NOT NULL DEFAULT '0'", + "mailbox_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", + "domain_relayhost" => "TINYINT(1) NOT NULL DEFAULT '1'", + "domain_desc" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "da_sso" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "token" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + ), + "keys" => array( + "primary" => array( + "" => array("token", "created") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "imapsync" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "user2" => "VARCHAR(255) NOT NULL", + "host1" => "VARCHAR(255) NOT NULL", + "authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'", + "regextrans2" => "VARCHAR(255) DEFAULT ''", + "authmd51" => "TINYINT(1) NOT NULL DEFAULT 0", + "domain2" => "VARCHAR(255) NOT NULL DEFAULT ''", + "subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''", + "user1" => "VARCHAR(255) NOT NULL", + "password1" => "VARCHAR(255) NOT NULL", + "exclude" => "VARCHAR(500) NOT NULL DEFAULT ''", + "maxage" => "SMALLINT NOT NULL DEFAULT '0'", + "mins_interval" => "SMALLINT UNSIGNED NOT NULL DEFAULT '0'", + "maxbytespersecond" => "VARCHAR(50) NOT NULL DEFAULT '0'", + "port1" => "SMALLINT UNSIGNED NOT NULL", + "enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'", + "delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'", + "delete1" => "TINYINT(1) NOT NULL DEFAULT '0'", + "delete2" => "TINYINT(1) NOT NULL DEFAULT '0'", + "automap" => "TINYINT(1) NOT NULL DEFAULT '0'", + "skipcrossduplicates" => "TINYINT(1) NOT NULL DEFAULT '0'", + "custom_params" => "VARCHAR(512) NOT NULL DEFAULT ''", + "timeout1" => "SMALLINT NOT NULL DEFAULT '600'", + "timeout2" => "SMALLINT NOT NULL DEFAULT '600'", + "subscribeall" => "TINYINT(1) NOT NULL DEFAULT '1'", + "is_running" => "TINYINT(1) NOT NULL DEFAULT '0'", + "returned_text" => "LONGTEXT", + "last_run" => "TIMESTAMP NULL DEFAULT NULL", + "success" => "TINYINT(1) UNSIGNED DEFAULT NULL", + "exit_status" => "VARCHAR(50) DEFAULT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "bcc_maps" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "local_dest" => "VARCHAR(255) NOT NULL", + "bcc_dest" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "type" => "ENUM('sender','rcpt')", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "local_dest" => array("local_dest"), + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "recipient_maps" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "old_dest" => "VARCHAR(255) NOT NULL", + "new_dest" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "local_dest" => array("old_dest"), + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "tfa" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "key_id" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL", + "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')", + "secret" => "VARCHAR(255) DEFAULT NULL", + "keyHandle" => "VARCHAR(1023) DEFAULT NULL", + "publicKey" => "VARCHAR(4096) DEFAULT NULL", + "counter" => "INT NOT NULL DEFAULT '0'", + "certificate" => "TEXT", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "forwarding_hosts" => array( + "cols" => array( + "host" => "VARCHAR(255) NOT NULL", + "source" => "VARCHAR(255) NOT NULL", + "filter_spam" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("host") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_acl" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "c_folder_id" => "INT NOT NULL", + "c_object" => "VARCHAR(255) NOT NULL", + "c_uid" => "VARCHAR(255) NOT NULL", + "c_role" => "VARCHAR(80) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ), + "key" => array( + "sogo_acl_c_folder_id_idx" => array("c_folder_id"), + "sogo_acl_c_uid_idx" => array("c_uid") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_alarms_folder" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "c_path" => "VARCHAR(255) NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_uid" => "VARCHAR(255) NOT NULL", + "c_recurrence_id" => "INT(11) DEFAULT NULL", + "c_alarm_number" => "INT(11) NOT NULL", + "c_alarm_date" => "INT(11) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_cache_folder" => array( + "cols" => array( + "c_uid" => "VARCHAR(255) NOT NULL", + "c_path" => "VARCHAR(255) NOT NULL", + "c_parent_path" => "VARCHAR(255) DEFAULT NULL", + "c_type" => "TINYINT(3) unsigned NOT NULL", + "c_creationdate" => "INT(11) NOT NULL", + "c_lastmodified" => "INT(11) NOT NULL", + "c_version" => "INT(11) NOT NULL DEFAULT '0'", + "c_deleted" => "TINYINT(4) NOT NULL DEFAULT '0'", + "c_content" => "LONGTEXT" + ), + "keys" => array( + "primary" => array( + "" => array("c_uid", "c_path") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_folder_info" => array( + "cols" => array( + "c_folder_id" => "BIGINT(20) unsigned NOT NULL AUTO_INCREMENT", + "c_path" => "VARCHAR(255) NOT NULL", + "c_path1" => "VARCHAR(255) NOT NULL", + "c_path2" => "VARCHAR(255) DEFAULT NULL", + "c_path3" => "VARCHAR(255) DEFAULT NULL", + "c_path4" => "VARCHAR(255) DEFAULT NULL", + "c_foldername" => "VARCHAR(255) NOT NULL", + "c_location" => "VARCHAR(2048) DEFAULT NULL", + "c_quick_location" => "VARCHAR(2048) DEFAULT NULL", + "c_acl_location" => "VARCHAR(2048) DEFAULT NULL", + "c_folder_type" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("c_path") + ), + "unique" => array( + "c_folder_id" => array("c_folder_id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_quick_appointment" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_uid" => "VARCHAR(1000) NOT NULL", + "c_startdate" => "INT", + "c_enddate" => "INT", + "c_cycleenddate" => "INT", + "c_title" => "VARCHAR(1000) NOT NULL", + "c_participants" => "TEXT", + "c_isallday" => "INT", + "c_iscycle" => "INT", + "c_cycleinfo" => "TEXT", + "c_classification" => "INT NOT NULL", + "c_isopaque" => "INT NOT NULL", + "c_status" => "INT NOT NULL", + "c_priority" => "INT", + "c_location" => "VARCHAR(255)", + "c_orgmail" => "VARCHAR(255)", + "c_partmails" => "TEXT", + "c_partstates" => "TEXT", + "c_category" => "VARCHAR(255)", + "c_sequence" => "INT", + "c_component" => "VARCHAR(10) NOT NULL", + "c_nextalarm" => "INT", + "c_description" => "TEXT" + ), + "keys" => array( + "primary" => array( + "" => array("c_folder_id", "c_name") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_quick_contact" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_givenname" => "VARCHAR(255)", + "c_cn" => "VARCHAR(255)", + "c_sn" => "VARCHAR(255)", + "c_screenname" => "VARCHAR(255)", + "c_l" => "VARCHAR(255)", + "c_mail" => "TEXT", + "c_o" => "VARCHAR(500)", + "c_ou" => "VARCHAR(255)", + "c_telephonenumber" => "VARCHAR(255)", + "c_categories" => "VARCHAR(255)", + "c_component" => "VARCHAR(10) NOT NULL", + "c_hascertificate" => "INT4 DEFAULT 0" + ), + "keys" => array( + "primary" => array( + "" => array("c_folder_id", "c_name") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_sessions_folder" => array( + "cols" => array( + "c_id" => "VARCHAR(255) NOT NULL", + "c_value" => "VARCHAR(4096) NOT NULL", + "c_creationdate" => "INT(11) NOT NULL", + "c_lastseen" => "INT(11) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("c_id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_store" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_content" => "MEDIUMTEXT NOT NULL", + "c_creationdate" => "INT NOT NULL", + "c_lastmodified" => "INT NOT NULL", + "c_version" => "INT NOT NULL", + "c_deleted" => "INT" + ), + "keys" => array( + "primary" => array( + "" => array("c_folder_id", "c_name") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "pushover" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "key" => "VARCHAR(255) NOT NULL", + "token" => "VARCHAR(255) NOT NULL", + "attributes" => "JSON", + "title" => "TEXT", + "text" => "TEXT", + "senders" => "TEXT", + "senders_regex" => "TEXT", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_user_profile" => array( + "cols" => array( + "c_uid" => "VARCHAR(255) NOT NULL", + "c_defaults" => "LONGTEXT", + "c_settings" => "LONGTEXT" + ), + "keys" => array( + "primary" => array( + "" => array("c_uid") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "oauth_clients" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "client_id" => "VARCHAR(80) NOT NULL", + "client_secret" => "VARCHAR(80)", + "redirect_uri" => "VARCHAR(2000)", + "grant_types" => "VARCHAR(80)", + "scope" => "VARCHAR(4000)", + "user_id" => "VARCHAR(80)" + ), + "keys" => array( + "primary" => array( + "" => array("client_id") + ), + "unique" => array( + "id" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "oauth_access_tokens" => array( + "cols" => array( + "access_token" => "VARCHAR(40) NOT NULL", + "client_id" => "VARCHAR(80) NOT NULL", + "user_id" => "VARCHAR(80)", + "expires" => "TIMESTAMP NOT NULL", + "scope" => "VARCHAR(4000)" + ), + "keys" => array( + "primary" => array( + "" => array("access_token") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "oauth_authorization_codes" => array( + "cols" => array( + "authorization_code" => "VARCHAR(40) NOT NULL", + "client_id" => "VARCHAR(80) NOT NULL", + "user_id" => "VARCHAR(80)", + "redirect_uri" => "VARCHAR(2000)", + "expires" => "TIMESTAMP NOT NULL", + "scope" => "VARCHAR(4000)", + "id_token" => "VARCHAR(1000)" + ), + "keys" => array( + "primary" => array( + "" => array("authorization_code") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "oauth_refresh_tokens" => array( + "cols" => array( + "refresh_token" => "VARCHAR(40) NOT NULL", + "client_id" => "VARCHAR(80) NOT NULL", + "user_id" => "VARCHAR(80)", + "expires" => "TIMESTAMP NOT NULL", + "scope" => "VARCHAR(4000)" + ), + "keys" => array( + "primary" => array( + "" => array("refresh_token") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ) + ); + + foreach ($tables as $table => $properties) { + // Migrate to quarantine + if ($table == 'quarantine') { + $stmt = $pdo->query("SHOW TABLES LIKE 'quarantaine'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->query("SHOW TABLES LIKE 'quarantine'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 0) { + $pdo->query("RENAME TABLE `quarantaine` TO `quarantine`"); + } + } + } + // Migrate tls_enforce_* options - if ($table == 'mailbox') { - $stmt = $pdo->query("SHOW TABLES LIKE 'mailbox'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE '%tls_enforce%'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->query("SELECT `username`, `tls_enforce_in`, `tls_enforce_out` FROM `mailbox`"); - $tls_options_rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row = array_shift($tls_options_rows)) { - $tls_options[$row['username']] = array('tls_enforce_in' => $row['tls_enforce_in'], 'tls_enforce_out' => $row['tls_enforce_out']); - } - } - } - } - - $stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $stmt = $pdo->prepare("SELECT CONCAT('ALTER TABLE ', `table_schema`, '.', `table_name`, ' DROP FOREIGN KEY ', `constraint_name`, ';') AS `FKEY_DROP` FROM `information_schema`.`table_constraints` - WHERE `constraint_type` = 'FOREIGN KEY' AND `table_name` = :table;"); - $stmt->execute(array(':table' => $table)); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row = array_shift($rows)) { - $pdo->query($row['FKEY_DROP']); - } - foreach($properties['cols'] as $column => $type) { - $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "` LIKE '" . $column . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - if (strpos($type, 'AUTO_INCREMENT') !== false) { - $type = $type . ' PRIMARY KEY '; - // Adding an AUTO_INCREMENT key, need to drop primary keys first, if exists - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); - } - } - $pdo->query("ALTER TABLE `" . $table . "` ADD `" . $column . "` " . $type); - } - else { - $pdo->query("ALTER TABLE `" . $table . "` MODIFY COLUMN `" . $column . "` " . $type); - } - } - foreach($properties['keys'] as $key_type => $key_content) { - if (strtolower($key_type) == 'primary') { - foreach ($key_content as $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - $is_drop = ($num_results != 0) ? "DROP PRIMARY KEY, " : ""; - $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD PRIMARY KEY (" . $fields . ")"); - } - } - if (strtolower($key_type) == 'key') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; - $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD KEY `" . $key_name . "` (" . $fields . ")"); - } - } - if (strtolower($key_type) == 'unique') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; - $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD UNIQUE KEY `" . $key_name . "` (" . $fields . ")"); - } - } - if (strtolower($key_type) == 'fkey') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $pdo->query("ALTER TABLE `" . $table . "` DROP INDEX `" . $key_name . "`"); - } - @list($table_ref, $field_ref) = explode('.', $key_values['ref']); - $pdo->query("ALTER TABLE `" . $table . "` ADD FOREIGN KEY `" . $key_name . "` (" . $key_values['col'] . ") REFERENCES `" . $table_ref . "` (`" . $field_ref . "`) - ON DELETE " . $key_values['delete'] . " ON UPDATE " . $key_values['update']); - } - } - } - // Drop all vanished columns - $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "`"); - $cols_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); - while ($row = array_shift($cols_in_table)) { - if (!array_key_exists($row['Field'], $properties['cols'])) { - $pdo->query("ALTER TABLE `" . $table . "` DROP COLUMN `" . $row['Field'] . "`;"); - } - } - - // Step 1: Get all non-primary keys, that currently exist and those that should exist - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE `Key_name` != 'PRIMARY'"); - $keys_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); - $keys_to_exist = array(); - if (isset($properties['keys']['unique']) && is_array($properties['keys']['unique'])) { - foreach ($properties['keys']['unique'] as $key_name => $key_values) { - $keys_to_exist[] = $key_name; - } - } - if (isset($properties['keys']['key']) && is_array($properties['keys']['key'])) { - foreach ($properties['keys']['key'] as $key_name => $key_values) { - $keys_to_exist[] = $key_name; - } - } - // Index for foreign key must exist - if (isset($properties['keys']['fkey']) && is_array($properties['keys']['fkey'])) { - foreach ($properties['keys']['fkey'] as $key_name => $key_values) { - $keys_to_exist[] = $key_name; - } - } - // Step 2: Drop all vanished indexes - while ($row = array_shift($keys_in_table)) { - if (!in_array($row['Key_name'], $keys_to_exist)) { - $pdo->query("ALTER TABLE `" . $table . "` DROP INDEX `" . $row['Key_name'] . "`"); - } - } - // Step 3: Drop all vanished primary keys - if (!isset($properties['keys']['primary'])) { - $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results != 0) { - $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); - } - } - } - else { - // Create table if it is missing - $sql = "CREATE TABLE IF NOT EXISTS `" . $table . "` ("; - foreach($properties['cols'] as $column => $type) { - $sql .= "`" . $column . "` " . $type . ","; - } - foreach($properties['keys'] as $key_type => $key_content) { - if (strtolower($key_type) == 'primary') { - foreach ($key_content as $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $sql .= "PRIMARY KEY (" . $fields . ")" . ","; - } - } - elseif (strtolower($key_type) == 'key') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $sql .= "KEY `" . $key_name . "` (" . $fields . ")" . ","; - } - } - elseif (strtolower($key_type) == 'unique') { - foreach ($key_content as $key_name => $key_values) { - $fields = "`" . implode("`, `", $key_values) . "`"; - $sql .= "UNIQUE KEY `" . $key_name . "` (" . $fields . ")" . ","; - } - } - elseif (strtolower($key_type) == 'fkey') { - foreach ($key_content as $key_name => $key_values) { - @list($table_ref, $field_ref) = explode('.', $key_values['ref']); - $sql .= "FOREIGN KEY `" . $key_name . "` (" . $key_values['col'] . ") REFERENCES `" . $table_ref . "` (`" . $field_ref . "`) - ON DELETE " . $key_values['delete'] . " ON UPDATE " . $key_values['update'] . ","; - } - } - } - $sql = rtrim($sql, ","); - $sql .= ") " . $properties['attr']; - $pdo->query($sql); - } - // Reset table attributes - $pdo->query("ALTER TABLE `" . $table . "` " . $properties['attr'] . ";"); - - } - - // Recreate SQL views - foreach ($views as $view => $create) { - $pdo->query("DROP VIEW IF EXISTS `" . $view . "`;"); - $pdo->query($create); - } - - // Mitigate imapsync argument injection issue - $pdo->query("UPDATE `imapsync` SET `custom_params` = '' - WHERE `custom_params` LIKE '%pipemess%' - OR custom_params LIKE '%skipmess%' - OR custom_params LIKE '%delete2foldersonly%' - OR custom_params LIKE '%delete2foldersbutnot%' - OR custom_params LIKE '%regexflag%' - OR custom_params LIKE '%pipemess%' - OR custom_params LIKE '%regextrans2%' - OR custom_params LIKE '%maxlinelengthcmd%';"); - - // Migrate webauthn tfa - $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')"); - - // Inject admin if not exists - $stmt = $pdo->query("SELECT NULL FROM `admin`"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`) - VALUES ('admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, NOW(), NOW(), 1)"); - $pdo->query("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) - SELECT `username`, 'ALL', NOW(), 1 FROM `admin` - WHERE superadmin='1' AND `username` NOT IN (SELECT `username` FROM `domain_admins`);"); - $pdo->query("DELETE FROM `admin` WHERE `username` NOT IN (SELECT `username` FROM `domain_admins`);"); - } - // Insert new DB schema version - $pdo->query("REPLACE INTO `versions` (`application`, `version`) VALUES ('db_schema', '" . $db_version . "');"); - - // Fix dangling domain admins - $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);"); - $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);"); - - // Migrate attributes - // pushover - $pdo->query("UPDATE `pushover` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); - $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.evaluate_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.evaluate_x_prio') IS NULL;"); - $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.only_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.only_x_prio') IS NULL;"); - $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.sound', \"pushover\") WHERE JSON_VALUE(`attributes`, '$.sound') IS NULL;"); - // mailbox - $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.passwd_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.passwd_update') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.relayhost', \"0\") WHERE JSON_VALUE(`attributes`, '$.relayhost') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sieve_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sieve_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.pop3_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.pop3_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_VALUE(`attributes`, '$.mailbox_format') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_VALUE(`attributes`, '$.quarantine_notification') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;"); - foreach($tls_options as $tls_user => $tls_options) { - $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_enforce_in), - `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_enforce_out) - WHERE `username` = :username"); - $stmt->execute(array(':tls_enforce_in' => $tls_options['tls_enforce_in'], ':tls_enforce_out' => $tls_options['tls_enforce_out'], ':username' => $tls_user)); - } - // Set tls_enforce_* if still missing (due to deleted attrs for example) - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', \"1\") WHERE JSON_VALUE(`attributes`, '$.tls_enforce_out') IS NULL;"); - $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', \"1\") WHERE JSON_VALUE(`attributes`, '$.tls_enforce_in') IS NULL;"); - // Fix ACL - $pdo->query("INSERT INTO `user_acl` (`username`) SELECT `username` FROM `mailbox` WHERE `kind` = '' AND NOT EXISTS (SELECT `username` FROM `user_acl`);"); - $pdo->query("INSERT INTO `da_acl` (`username`) SELECT DISTINCT `username` FROM `domain_admins` WHERE `username` != 'admin' AND NOT EXISTS (SELECT `username` FROM `da_acl`);"); - // Fix domain_admins - $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';"); - + if ($table == 'mailbox') { + $stmt = $pdo->query("SHOW TABLES LIKE 'mailbox'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE '%tls_enforce%'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->query("SELECT `username`, `tls_enforce_in`, `tls_enforce_out` FROM `mailbox`"); + $tls_options_rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($tls_options_rows)) { + $tls_options[$row['username']] = array('tls_enforce_in' => $row['tls_enforce_in'], 'tls_enforce_out' => $row['tls_enforce_out']); + } + } + } + } + + $stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->prepare("SELECT CONCAT('ALTER TABLE ', `table_schema`, '.', `table_name`, ' DROP FOREIGN KEY ', `constraint_name`, ';') AS `FKEY_DROP` FROM `information_schema`.`table_constraints` + WHERE `constraint_type` = 'FOREIGN KEY' AND `table_name` = :table;"); + $stmt->execute(array(':table' => $table)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($rows)) { + $pdo->query($row['FKEY_DROP']); + } + foreach($properties['cols'] as $column => $type) { + $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "` LIKE '" . $column . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 0) { + if (strpos($type, 'AUTO_INCREMENT') !== false) { + $type = $type . ' PRIMARY KEY '; + // Adding an AUTO_INCREMENT key, need to drop primary keys first, if exists + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); + } + } + $pdo->query("ALTER TABLE `" . $table . "` ADD `" . $column . "` " . $type); + } + else { + $pdo->query("ALTER TABLE `" . $table . "` MODIFY COLUMN `" . $column . "` " . $type); + } + } + foreach($properties['keys'] as $key_type => $key_content) { + if (strtolower($key_type) == 'primary') { + foreach ($key_content as $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + $is_drop = ($num_results != 0) ? "DROP PRIMARY KEY, " : ""; + $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD PRIMARY KEY (" . $fields . ")"); + } + } + if (strtolower($key_type) == 'key') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; + $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD KEY `" . $key_name . "` (" . $fields . ")"); + } + } + if (strtolower($key_type) == 'unique') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; + $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD UNIQUE KEY `" . $key_name . "` (" . $fields . ")"); + } + } + if (strtolower($key_type) == 'fkey') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $pdo->query("ALTER TABLE `" . $table . "` DROP INDEX `" . $key_name . "`"); + } + @list($table_ref, $field_ref) = explode('.', $key_values['ref']); + $pdo->query("ALTER TABLE `" . $table . "` ADD FOREIGN KEY `" . $key_name . "` (" . $key_values['col'] . ") REFERENCES `" . $table_ref . "` (`" . $field_ref . "`) + ON DELETE " . $key_values['delete'] . " ON UPDATE " . $key_values['update']); + } + } + } + // Drop all vanished columns + $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "`"); + $cols_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($cols_in_table)) { + if (!array_key_exists($row['Field'], $properties['cols'])) { + $pdo->query("ALTER TABLE `" . $table . "` DROP COLUMN `" . $row['Field'] . "`;"); + } + } + + // Step 1: Get all non-primary keys, that currently exist and those that should exist + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE `Key_name` != 'PRIMARY'"); + $keys_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); + $keys_to_exist = array(); + if (isset($properties['keys']['unique']) && is_array($properties['keys']['unique'])) { + foreach ($properties['keys']['unique'] as $key_name => $key_values) { + $keys_to_exist[] = $key_name; + } + } + if (isset($properties['keys']['key']) && is_array($properties['keys']['key'])) { + foreach ($properties['keys']['key'] as $key_name => $key_values) { + $keys_to_exist[] = $key_name; + } + } + // Index for foreign key must exist + if (isset($properties['keys']['fkey']) && is_array($properties['keys']['fkey'])) { + foreach ($properties['keys']['fkey'] as $key_name => $key_values) { + $keys_to_exist[] = $key_name; + } + } + // Step 2: Drop all vanished indexes + while ($row = array_shift($keys_in_table)) { + if (!in_array($row['Key_name'], $keys_to_exist)) { + $pdo->query("ALTER TABLE `" . $table . "` DROP INDEX `" . $row['Key_name'] . "`"); + } + } + // Step 3: Drop all vanished primary keys + if (!isset($properties['keys']['primary'])) { + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); + } + } + } + else { + // Create table if it is missing + $sql = "CREATE TABLE IF NOT EXISTS `" . $table . "` ("; + foreach($properties['cols'] as $column => $type) { + $sql .= "`" . $column . "` " . $type . ","; + } + foreach($properties['keys'] as $key_type => $key_content) { + if (strtolower($key_type) == 'primary') { + foreach ($key_content as $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $sql .= "PRIMARY KEY (" . $fields . ")" . ","; + } + } + elseif (strtolower($key_type) == 'key') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $sql .= "KEY `" . $key_name . "` (" . $fields . ")" . ","; + } + } + elseif (strtolower($key_type) == 'unique') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $sql .= "UNIQUE KEY `" . $key_name . "` (" . $fields . ")" . ","; + } + } + elseif (strtolower($key_type) == 'fkey') { + foreach ($key_content as $key_name => $key_values) { + @list($table_ref, $field_ref) = explode('.', $key_values['ref']); + $sql .= "FOREIGN KEY `" . $key_name . "` (" . $key_values['col'] . ") REFERENCES `" . $table_ref . "` (`" . $field_ref . "`) + ON DELETE " . $key_values['delete'] . " ON UPDATE " . $key_values['update'] . ","; + } + } + } + $sql = rtrim($sql, ","); + $sql .= ") " . $properties['attr']; + $pdo->query($sql); + } + // Reset table attributes + $pdo->query("ALTER TABLE `" . $table . "` " . $properties['attr'] . ";"); + + } + + // Recreate SQL views + foreach ($views as $view => $create) { + $pdo->query("DROP VIEW IF EXISTS `" . $view . "`;"); + $pdo->query($create); + } + + // Mitigate imapsync argument injection issue + $pdo->query("UPDATE `imapsync` SET `custom_params` = '' + WHERE `custom_params` LIKE '%pipemess%' + OR custom_params LIKE '%skipmess%' + OR custom_params LIKE '%delete2foldersonly%' + OR custom_params LIKE '%delete2foldersbutnot%' + OR custom_params LIKE '%regexflag%' + OR custom_params LIKE '%pipemess%' + OR custom_params LIKE '%regextrans2%' + OR custom_params LIKE '%maxlinelengthcmd%';"); + + // Migrate webauthn tfa + $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')"); + + // Inject admin if not exists + $stmt = $pdo->query("SELECT NULL FROM `admin`"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 0) { + $pdo->query("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`) + VALUES ('admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, NOW(), NOW(), 1)"); + $pdo->query("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + SELECT `username`, 'ALL', NOW(), 1 FROM `admin` + WHERE superadmin='1' AND `username` NOT IN (SELECT `username` FROM `domain_admins`);"); + $pdo->query("DELETE FROM `admin` WHERE `username` NOT IN (SELECT `username` FROM `domain_admins`);"); + } + // Insert new DB schema version + $pdo->query("REPLACE INTO `versions` (`application`, `version`) VALUES ('db_schema', '" . $db_version . "');"); + + // Fix dangling domain admins + $pdo->query("DELETE FROM `admin` WHERE `superadmin` = 0 AND `username` NOT IN (SELECT `username`FROM `domain_admins`);"); + $pdo->query("DELETE FROM `da_acl` WHERE `username` NOT IN (SELECT `username`FROM `domain_admins`);"); + + // Migrate attributes + // pushover + $pdo->query("UPDATE `pushover` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); + $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.evaluate_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.evaluate_x_prio') IS NULL;"); + $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.only_x_prio', \"0\") WHERE JSON_VALUE(`attributes`, '$.only_x_prio') IS NULL;"); + $pdo->query("UPDATE `pushover` SET `attributes` = JSON_SET(`attributes`, '$.sound', \"pushover\") WHERE JSON_VALUE(`attributes`, '$.sound') IS NULL;"); + // mailbox + $pdo->query("UPDATE `mailbox` SET `attributes` = '{}' WHERE `attributes` = '' OR `attributes` IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.passwd_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.passwd_update') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.relayhost', \"0\") WHERE JSON_VALUE(`attributes`, '$.relayhost') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.force_pw_update', \"0\") WHERE JSON_VALUE(`attributes`, '$.force_pw_update') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sieve_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sieve_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.sogo_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.sogo_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.imap_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.imap_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.pop3_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.pop3_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.smtp_access', \"1\") WHERE JSON_VALUE(`attributes`, '$.smtp_access') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.mailbox_format', \"maildir:\") WHERE JSON_VALUE(`attributes`, '$.mailbox_format') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_notification', \"never\") WHERE JSON_VALUE(`attributes`, '$.quarantine_notification') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.quarantine_category', \"reject\") WHERE JSON_VALUE(`attributes`, '$.quarantine_category') IS NULL;"); + foreach($tls_options as $tls_user => $tls_options) { + $stmt = $pdo->prepare("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', :tls_enforce_in), + `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', :tls_enforce_out) + WHERE `username` = :username"); + $stmt->execute(array(':tls_enforce_in' => $tls_options['tls_enforce_in'], ':tls_enforce_out' => $tls_options['tls_enforce_out'], ':username' => $tls_user)); + } + // Set tls_enforce_* if still missing (due to deleted attrs for example) + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_out', \"1\") WHERE JSON_VALUE(`attributes`, '$.tls_enforce_out') IS NULL;"); + $pdo->query("UPDATE `mailbox` SET `attributes` = JSON_SET(`attributes`, '$.tls_enforce_in', \"1\") WHERE JSON_VALUE(`attributes`, '$.tls_enforce_in') IS NULL;"); + // Fix ACL + $pdo->query("INSERT INTO `user_acl` (`username`) SELECT `username` FROM `mailbox` WHERE `kind` = '' AND NOT EXISTS (SELECT `username` FROM `user_acl`);"); + $pdo->query("INSERT INTO `da_acl` (`username`) SELECT DISTINCT `username` FROM `domain_admins` WHERE `username` != 'admin' AND NOT EXISTS (SELECT `username` FROM `da_acl`);"); + // Fix domain_admins + $pdo->query("DELETE FROM `domain_admins` WHERE `domain` = 'ALL';"); + // add default templates $default_domain_template = array( "template" => "Default", @@ -1398,68 +1411,68 @@ function init_db_schema() { )); } - if (php_sapi_name() == "cli") { - echo "DB initialization completed" . PHP_EOL; - } else { - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__), - 'msg' => 'db_init_complete' - ); - } - } - catch (PDOException $e) { - if (php_sapi_name() == "cli") { - echo "DB initialization failed: " . print_r($e, true) . PHP_EOL; - } else { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__), - 'msg' => array('mysql_error', $e) - ); - } - } -} -if (php_sapi_name() == "cli") { - include '/web/inc/vars.inc.php'; - include '/web/inc/functions.docker.inc.php'; - // $now = new DateTime(); - // $mins = $now->getOffset() / 60; - // $sgn = ($mins < 0 ? -1 : 1); - // $mins = abs($mins); - // $hrs = floor($mins / 60); - // $mins -= $hrs * 60; - // $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins); - $dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name; - $opt = [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - PDO::ATTR_EMULATE_PREPARES => false, - //PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;", - ]; - $pdo = new PDO($dsn, $database_user, $database_pass, $opt); - $stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view';"); - $res = $stmt->fetch(PDO::FETCH_ASSOC); - if (intval($res['OK_C']) === 2) { - // Be more precise when replacing into _sogo_static_view, col orders may change - try { - $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`) - SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view"); - $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');"); - echo "Fixed _sogo_static_view" . PHP_EOL; - } - catch ( Exception $e ) { - // Dunno - } - } - try { - $m = new Memcached(); - $m->addServer('memcached', 11211); - $m->flush(); - echo "Cleaned up memcached". PHP_EOL; - } - catch ( Exception $e ) { - // Dunno - } - init_db_schema(); -} + if (php_sapi_name() == "cli") { + echo "DB initialization completed" . PHP_EOL; + } else { + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__), + 'msg' => 'db_init_complete' + ); + } + } + catch (PDOException $e) { + if (php_sapi_name() == "cli") { + echo "DB initialization failed: " . print_r($e, true) . PHP_EOL; + } else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__), + 'msg' => array('mysql_error', $e) + ); + } + } +} +if (php_sapi_name() == "cli") { + include '/web/inc/vars.inc.php'; + include '/web/inc/functions.docker.inc.php'; + // $now = new DateTime(); + // $mins = $now->getOffset() / 60; + // $sgn = ($mins < 0 ? -1 : 1); + // $mins = abs($mins); + // $hrs = floor($mins / 60); + // $mins -= $hrs * 60; + // $offset = sprintf('%+d:%02d', $hrs*$sgn, $mins); + $dsn = $database_type . ":unix_socket=" . $database_sock . ";dbname=" . $database_name; + $opt = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + //PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "', group_concat_max_len = 3423543543;", + ]; + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + $stmt = $pdo->query("SELECT COUNT('OK') AS OK_C FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'sogo_view' OR TABLE_NAME = '_sogo_static_view';"); + $res = $stmt->fetch(PDO::FETCH_ASSOC); + if (intval($res['OK_C']) === 2) { + // Be more precise when replacing into _sogo_static_view, col orders may change + try { + $stmt = $pdo->query("REPLACE INTO _sogo_static_view (`c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings`) + SELECT `c_uid`, `domain`, `c_name`, `c_password`, `c_cn`, `mail`, `aliases`, `ad_aliases`, `ext_acl`, `kind`, `multiple_bookings` from sogo_view"); + $stmt = $pdo->query("DELETE FROM _sogo_static_view WHERE `c_uid` NOT IN (SELECT `username` FROM `mailbox` WHERE `active` = '1');"); + echo "Fixed _sogo_static_view" . PHP_EOL; + } + catch ( Exception $e ) { + // Dunno + } + } + try { + $m = new Memcached(); + $m->addServer('memcached', 11211); + $m->flush(); + echo "Cleaned up memcached". PHP_EOL; + } + catch ( Exception $e ) { + // Dunno + } + init_db_schema(); +} diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index 5c7ec710..1a33e760 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -1,140 +1,140 @@ - $SESSION_LIFETIME)) { - session_unset(); - session_destroy(); -} -$_SESSION['LAST_ACTIVITY'] = time(); - -// API -if (!empty($_SERVER['HTTP_X_API_KEY'])) { - $stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';"); - $stmt->execute(array( - ':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY']) - )); - $api_return = $stmt->fetch(PDO::FETCH_ASSOC); - if (!empty($api_return['api_key'])) { - $skip_ip_check = ($api_return['skip_ip_check'] == 1); - $remote = get_remote_ip(false); - $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from'])); - if ($skip_ip_check === true || ip_acl($remote, $allow_from)) { - $_SESSION['mailcow_cc_username'] = 'API'; - $_SESSION['mailcow_cc_role'] = 'admin'; - $_SESSION['mailcow_cc_api'] = true; - if ($api_return['access'] == 'rw') { - $_SESSION['mailcow_cc_api_access'] = 'rw'; - } - else { - $_SESSION['mailcow_cc_api_access'] = 'ro'; - } - } - else { - $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); - error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); - http_response_code(401); - echo json_encode(array( - 'type' => 'error', - 'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR'] - )); - unset($_POST); - exit(); - } - } - else { - $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); - error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); - http_response_code(401); - echo json_encode(array( - 'type' => 'error', - 'msg' => 'authentication failed' - )); - unset($_POST); - exit(); - } -} - -// Handle logouts -if (isset($_POST["logout"])) { - if (isset($_SESSION["dual-login"])) { - $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"]; - $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"]; - unset($_SESSION["dual-login"]); - header("Location: /mailbox"); - exit(); - } - else { - session_regenerate_id(true); - session_unset(); - session_destroy(); - session_write_close(); - header("Location: /"); - } -} - -// Check session -function session_check() { - if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) { - return true; - } - if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) { - $_SESSION['return'][] = array( - 'type' => 'warning', - 'msg' => 'session_ua' - ); - return false; - } - if (!empty($_POST)) { - if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) { - $_SESSION['return'][] = array( - 'type' => 'warning', - 'msg' => 'session_token' - ); - return false; - } - unset($_POST['csrf_token']); - $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32)); - $_SESSION['CSRF']['TIME'] = time(); - } - return true; -} - -if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) { - $_POST = array(); - $_FILES = array(); -} + $SESSION_LIFETIME)) { + session_unset(); + session_destroy(); +} +$_SESSION['LAST_ACTIVITY'] = time(); + +// API +if (!empty($_SERVER['HTTP_X_API_KEY'])) { + $stmt = $pdo->prepare("SELECT * FROM `api` WHERE `api_key` = :api_key AND `active` = '1';"); + $stmt->execute(array( + ':api_key' => preg_replace('/[^a-zA-Z0-9-]/', '', $_SERVER['HTTP_X_API_KEY']) + )); + $api_return = $stmt->fetch(PDO::FETCH_ASSOC); + if (!empty($api_return['api_key'])) { + $skip_ip_check = ($api_return['skip_ip_check'] == 1); + $remote = get_remote_ip(false); + $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from'])); + if ($skip_ip_check === true || ip_acl($remote, $allow_from)) { + $_SESSION['mailcow_cc_username'] = 'API'; + $_SESSION['mailcow_cc_role'] = 'admin'; + $_SESSION['mailcow_cc_api'] = true; + if ($api_return['access'] == 'rw') { + $_SESSION['mailcow_cc_api_access'] = 'rw'; + } + else { + $_SESSION['mailcow_cc_api_access'] = 'ro'; + } + } + else { + $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); + error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); + http_response_code(401); + echo json_encode(array( + 'type' => 'error', + 'msg' => 'api access denied for ip ' . $_SERVER['REMOTE_ADDR'] + )); + unset($_POST); + exit(); + } + } + else { + $redis->publish("F2B_CHANNEL", "mailcow UI: Invalid password for API_USER by " . $_SERVER['REMOTE_ADDR']); + error_log("mailcow UI: Invalid password for " . $user . " by " . $_SERVER['REMOTE_ADDR']); + http_response_code(401); + echo json_encode(array( + 'type' => 'error', + 'msg' => 'authentication failed' + )); + unset($_POST); + exit(); + } +} + +// Handle logouts +if (isset($_POST["logout"])) { + if (isset($_SESSION["dual-login"])) { + $_SESSION["mailcow_cc_username"] = $_SESSION["dual-login"]["username"]; + $_SESSION["mailcow_cc_role"] = $_SESSION["dual-login"]["role"]; + unset($_SESSION["dual-login"]); + header("Location: /mailbox"); + exit(); + } + else { + session_regenerate_id(true); + session_unset(); + session_destroy(); + session_write_close(); + header("Location: /"); + } +} + +// Check session +function session_check() { + if (isset($_SESSION['mailcow_cc_api']) && $_SESSION['mailcow_cc_api'] === true) { + return true; + } + if (!isset($_SESSION['SESS_REMOTE_UA']) || ($_SESSION['SESS_REMOTE_UA'] != $_SERVER['HTTP_USER_AGENT'])) { + $_SESSION['return'][] = array( + 'type' => 'warning', + 'msg' => 'session_ua' + ); + return false; + } + if (!empty($_POST)) { + if ($_SESSION['CSRF']['TOKEN'] != $_POST['csrf_token']) { + $_SESSION['return'][] = array( + 'type' => 'warning', + 'msg' => 'session_token' + ); + return false; + } + unset($_POST['csrf_token']); + $_SESSION['CSRF']['TOKEN'] = bin2hex(random_bytes(32)); + $_SESSION['CSRF']['TIME'] = time(); + } + return true; +} + +if (isset($_SESSION['mailcow_cc_role']) && session_check() === false) { + $_POST = array(); + $_FILES = array(); +} diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index aec043e9..fde1507f 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -1,4 +1,15 @@ Date: Thu, 26 Jan 2023 20:09:52 +0100 Subject: [PATCH 151/170] Translations update from Weblate (#5026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Web] Updated lang.en-gb.json Co-authored-by: Peter * [Web] Updated lang.de-de.json Co-authored-by: Peter * [Web] Updated lang.sk-sk.json Co-authored-by: Lukáš Matula Co-authored-by: milkmaker Co-authored-by: Peter Co-authored-by: Lukáš Matula --- data/web/lang/lang.de-de.json | 23 +++++++++++++++-------- data/web/lang/lang.en-gb.json | 2 +- data/web/lang/lang.sk-sk.json | 3 ++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index 4f7feb5b..f348ce6a 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -339,7 +339,8 @@ "oauth2_add_client": "Füge OAuth2 Client hinzu", "api_read_only": "Schreibgeschützter Zugriff", "api_read_write": "Lese-Schreib-Zugriff", - "oauth2_apps": "OAuth2 Apps" + "oauth2_apps": "OAuth2 Apps", + "queue_unban": "entsperren" }, "danger": { "access_denied": "Zugriff verweigert oder unvollständige/ungültige Daten", @@ -366,7 +367,7 @@ "domain_not_empty": "Domain %s ist nicht leer", "domain_not_found": "Domain %s nicht gefunden", "domain_quota_m_in_use": "Domain-Speicherplatzlimit muss größer oder gleich %d MiB sein", - "extended_sender_acl_denied": "Keine Rechte zum setzen von externen Absenderadressen", + "extended_sender_acl_denied": "Keine Rechte zum Setzen von externen Absenderadressen", "extra_acl_invalid": "Externe Absenderadresse \"%s\" ist ungültig", "extra_acl_invalid_domain": "Externe Absenderadresse \"%s\" verwendet eine ungültige Domain", "fido2_verification_failed": "FIDO2-Verifizierung fehlgeschlagen: %s", @@ -460,11 +461,14 @@ "username_invalid": "Benutzername %s kann nicht verwendet werden", "validity_missing": "Bitte geben Sie eine Gültigkeitsdauer an", "value_missing": "Bitte alle Felder ausfüllen", - "yotp_verification_failed": "Yubico OTP-Verifizierung fehlgeschlagen: %s" + "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" }, "datatables": { "collapse_all": "Alle Einklappen", - "decimal": "", + "decimal": ",", "emptyTable": "Keine Daten in der Tabelle vorhanden", "expand_all": "Alle Ausklappen", "info": "_START_ bis _END_ von _TOTAL_ Einträgen", @@ -498,7 +502,7 @@ "current_time": "Systemzeit", "disk_usage": "Festplattennutzung", "docs": "Dokumente", - "error_show_ip": "konnte die öffentlichen IP Adressen nicht auflösen", + "error_show_ip": "Konnte die öffentlichen IP Adressen nicht auflösen", "external_logs": "Externe Logs", "history_all_servers": "History (alle Server)", "in_memory_logs": "In-memory Logs", @@ -651,7 +655,8 @@ "title": "Objekt bearbeiten", "unchanged_if_empty": "Unverändert, wenn leer", "username": "Benutzername", - "validate_save": "Validieren und speichern" + "validate_save": "Validieren und speichern", + "pushover_sound": "Ton" }, "fido2": { "confirm": "Bestätigen", @@ -692,7 +697,8 @@ "quarantine": "Quarantäne", "restart_netfilter": "Netfilter neustarten", "restart_sogo": "SOGo neustarten", - "user_settings": "Benutzereinstellungen" + "user_settings": "Benutzereinstellungen", + "mailcow_system": "System" }, "info": { "awaiting_tfa_confirmation": "Warte auf TFA-Verifizierung", @@ -1236,7 +1242,8 @@ "syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem", "syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung", "syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem", - "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort" + "syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort", + "pushover_sound": "Ton" }, "warning": { "cannot_delete_self": "Kann derzeit eingeloggten Benutzer nicht entfernen", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index 5719049e..f7fc0577 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -468,7 +468,7 @@ }, "datatables": { "collapse_all": "Collapse All", - "decimal": "", + "decimal": ".", "emptyTable": "No data available in table", "expand_all": "Expand All", "info": "Showing _START_ to _END_ of _TOTAL_ entries", diff --git a/data/web/lang/lang.sk-sk.json b/data/web/lang/lang.sk-sk.json index b6933b28..f2f23681 100644 --- a/data/web/lang/lang.sk-sk.json +++ b/data/web/lang/lang.sk-sk.json @@ -106,7 +106,8 @@ "username": "Používateľské meno", "validate": "Overiť", "validation_success": "Úspešne overené", - "app_passwd_protocols": "Povolené protokoly k heslu aplikácie" + "app_passwd_protocols": "Povolené protokoly k heslu aplikácie", + "tags": "Štítky" }, "admin": { "access": "Prístup", From a1f033e4c167e2d87eafc4acfdbed4177285c3b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 30 Jan 2023 19:58:17 +0100 Subject: [PATCH 152/170] Update docker/build-push-action action to v4 (#5032) Signed-off-by: milkmaker Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/rebuild_backup_image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rebuild_backup_image.yml b/.github/workflows/rebuild_backup_image.yml index 120d68d9..21c218a8 100644 --- a/.github/workflows/rebuild_backup_image.yml +++ b/.github/workflows/rebuild_backup_image.yml @@ -26,7 +26,7 @@ jobs: password: ${{ secrets.BACKUPIMAGEBUILD_ACTION_DOCKERHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v4 with: context: . file: data/Dockerfiles/backup/Dockerfile From d62c27500430da91463e731cdf1ddb701e25366e Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 31 Jan 2023 09:49:18 +0100 Subject: [PATCH 153/170] [Web] match PAGINATION_SIZE to an existing datatable option --- data/web/inc/vars.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/inc/vars.inc.php b/data/web/inc/vars.inc.php index 4f09d5f9..5e6d72e7 100644 --- a/data/web/inc/vars.inc.php +++ b/data/web/inc/vars.inc.php @@ -124,7 +124,7 @@ $MAILCOW_APPS = array( ); // Rows until pagination begins -$PAGINATION_SIZE = 20; +$PAGINATION_SIZE = 25; // Default number of rows/lines to display (log table) $LOG_LINES = 1000; From 72e8180c6be0f8ae9444e4ae10129ffa830c0c9b Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 31 Jan 2023 10:37:51 +0100 Subject: [PATCH 154/170] [Web] datatable adjustment --- data/web/css/build/013-datatables.css | 20 +++++++++++++++++++- data/web/css/build/014-mailcow.css | 11 ----------- data/web/css/build/015-responsive.css | 3 +++ data/web/js/site/admin.js | 12 ++++++------ data/web/js/site/mailbox.js | 24 ++++++++++++------------ data/web/js/site/quarantine.js | 2 +- data/web/js/site/queue.js | 2 +- data/web/js/site/user.js | 6 +++--- 8 files changed, 45 insertions(+), 35 deletions(-) diff --git a/data/web/css/build/013-datatables.css b/data/web/css/build/013-datatables.css index e5518ff8..13378460 100644 --- a/data/web/css/build/013-datatables.css +++ b/data/web/css/build/013-datatables.css @@ -77,4 +77,22 @@ li .dtr-data { table.dataTable>tbody>tr.child span.dtr-title { width: 30%; max-width: 250px; -} \ No newline at end of file +} + + +div.dataTables_wrapper div.dataTables_filter { + text-align: left; +} +div.dataTables_wrapper div.dataTables_length { + text-align: right; +} +.dataTables_paginate, .dataTables_length, .dataTables_filter { + margin: 10px 0!important; +} + +td.dt-text-right { + text-align: end !important; +} +th.dt-text-right { + text-align: end !important; +} diff --git a/data/web/css/build/014-mailcow.css b/data/web/css/build/014-mailcow.css index 3d0eeaee..374d484d 100644 --- a/data/web/css/build/014-mailcow.css +++ b/data/web/css/build/014-mailcow.css @@ -370,14 +370,3 @@ button[aria-expanded='true'] > .caret { .btn-check:checked+.btn-outline-secondary, .btn-check:active+.btn-outline-secondary, .btn-outline-secondary:active, .btn-outline-secondary.active, .btn-outline-secondary.dropdown-toggle.show { background-color: #f0f0f0 !important; } - - -div.dataTables_wrapper div.dataTables_filter { - text-align: left; -} -div.dataTables_wrapper div.dataTables_length { - text-align: right; -} -.dataTables_paginate, .dataTables_length, .dataTables_filter { - margin: 10px 0!important; -} \ No newline at end of file diff --git a/data/web/css/build/015-responsive.css b/data/web/css/build/015-responsive.css index 47eadb53..a626a384 100644 --- a/data/web/css/build/015-responsive.css +++ b/data/web/css/build/015-responsive.css @@ -203,6 +203,9 @@ text-align: left; } + .senders-mw220 { + max-width: 100% !important; + } } @media (max-width: 350px) { diff --git a/data/web/js/site/admin.js b/data/web/js/site/admin.js index 0e5a9ae6..23ef1d25 100644 --- a/data/web/js/site/admin.js +++ b/data/web/js/site/admin.js @@ -133,7 +133,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-sm-head-hidden dt-text-right', defaultContent: '' }, ], @@ -204,7 +204,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-sm-head-hidden dt-text-right', defaultContent: '' }, ] @@ -277,7 +277,7 @@ jQuery(function($){ title: lang.action, data: 'action', defaultContent: '', - className: 'text-md-end dt-sm-head-hidden dt-body-right' + className: 'dt-sm-head-hidden dt-text-right' }, ] }); @@ -343,7 +343,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-sm-head-hidden dt-text-right', defaultContent: '' }, ] @@ -421,7 +421,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-sm-head-hidden dt-text-right', defaultContent: '' }, ] @@ -499,7 +499,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-sm-head-hidden dt-text-right', defaultContent: '' }, ] diff --git a/data/web/js/site/mailbox.js b/data/web/js/site/mailbox.js index 49cce1b2..2ef84688 100644 --- a/data/web/js/site/mailbox.js +++ b/data/web/js/site/mailbox.js @@ -613,7 +613,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 5, defaultContent: '' }, @@ -823,7 +823,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 6, defaultContent: '' }, @@ -1095,7 +1095,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 6, defaultContent: '' }, @@ -1326,7 +1326,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 6, defaultContent: '' }, @@ -1439,7 +1439,7 @@ jQuery(function($){ data: 'action', responsivePriority: 5, defaultContent: '', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right' + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right' }, ] }); @@ -1575,7 +1575,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 5, defaultContent: '' }, @@ -1672,7 +1672,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 5, defaultContent: '' }, @@ -1779,7 +1779,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 5, defaultContent: '' }, @@ -1933,7 +1933,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 5, defaultContent: '' }, @@ -2028,7 +2028,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 5, defaultContent: '' }, @@ -2181,7 +2181,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 5, defaultContent: '' }, @@ -2292,7 +2292,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-body-right', + className: 'dt-sm-head-hidden dt-data-w100 dtr-col-md dt-text-right', responsivePriority: 5, defaultContent: '' }, diff --git a/data/web/js/site/quarantine.js b/data/web/js/site/quarantine.js index e69863f7..18d7a1d5 100644 --- a/data/web/js/site/quarantine.js +++ b/data/web/js/site/quarantine.js @@ -163,7 +163,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-text-right dt-sm-head-hidden', defaultContent: '' }, ] diff --git a/data/web/js/site/queue.js b/data/web/js/site/queue.js index bc4c7369..f37884a6 100644 --- a/data/web/js/site/queue.js +++ b/data/web/js/site/queue.js @@ -116,7 +116,7 @@ jQuery(function($){ { title: lang_admin.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-sm-head-hidden dt-text-right', defaultContent: '' }, ] diff --git a/data/web/js/site/user.js b/data/web/js/site/user.js index d93e692f..b2139829 100644 --- a/data/web/js/site/user.js +++ b/data/web/js/site/user.js @@ -208,7 +208,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-sm-head-hidden dt-text-right', defaultContent: '' } ] @@ -363,7 +363,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-sm-head-hidden dt-text-right', defaultContent: '', responsivePriority: 5 } @@ -460,7 +460,7 @@ jQuery(function($){ { title: lang.action, data: 'action', - className: 'text-md-end dt-sm-head-hidden dt-body-right', + className: 'dt-sm-head-hidden dt-text-right', defaultContent: '' } ] From 64ac6a88911e4a644fad3bba17d6e40887447eea Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Tue, 31 Jan 2023 10:54:16 +0100 Subject: [PATCH 155/170] [Web] Skip update_sogo_static_view if sogo is disabled --- data/web/inc/functions.mailbox.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index f96894ff..4529ee7b 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -5264,7 +5264,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { } break; } - if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource'))) { + if ($_action != 'get' && in_array($_type, array('domain', 'alias', 'alias_domain', 'mailbox', 'resource')) && getenv('SKIP_SOGO') != "y") { update_sogo_static_view(); } } From 6aebb8352eabe0a144f4c3b48d038a6c11cb9bc7 Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Thu, 2 Feb 2023 11:03:51 +0100 Subject: [PATCH 156/170] [Fix] SOGo Update Fix for 5.8.0 (macOS fix) --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 44dc8f10..05a2f9aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -169,7 +169,7 @@ services: - phpfpm sogo-mailcow: - image: mailcow/sogo:1.114 + image: mailcow/sogo:1.115 environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} From e8fd34d31f4551c7415b38a8a401e619a9fe24e4 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Thu, 2 Feb 2023 11:28:51 +0100 Subject: [PATCH 157/170] [Web] webauthn add lang strings --- data/web/inc/functions.inc.php | 24 ++++++++++++------------ data/web/lang/lang.de-de.json | 3 +++ data/web/lang/lang.en-gb.json | 3 +++ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 3bab56bb..de1855fa 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -1739,7 +1739,7 @@ function verify_tfa_login($username, $_data) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), - 'msg' => array('webauthn_verification_failed', 'authenticator not found') + 'msg' => array('webauthn_authenticator_failed') ); return false; } @@ -1748,11 +1748,20 @@ function verify_tfa_login($username, $_data) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), - 'msg' => array('webauthn_verification_failed', 'publicKey not found') + 'msg' => array('webauthn_publickey_failed') ); return false; } + if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $username, '*'), + 'msg' => array('webauthn_username_failed') + ); + return false; + } + try { $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']); } @@ -1784,21 +1793,12 @@ function verify_tfa_login($username, $_data) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), - 'msg' => array('webauthn_verification_failed', 'could not determine user role') + 'msg' => array('webauthn_role_failed') ); return false; } } - if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){ - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $username, '*'), - 'msg' => array('webauthn_verification_failed', 'user who requests does not match with sql entry') - ); - return false; - } - $_SESSION["mailcow_cc_username"] = $process_webauthn['username']; $_SESSION['tfa_id'] = $process_webauthn['id']; $_SESSION['authReq'] = null; diff --git a/data/web/lang/lang.de-de.json b/data/web/lang/lang.de-de.json index f348ce6a..3d1e2d59 100644 --- a/data/web/lang/lang.de-de.json +++ b/data/web/lang/lang.de-de.json @@ -455,6 +455,9 @@ "totp_verification_failed": "TOTP-Verifizierung fehlgeschlagen", "transport_dest_exists": "Transport-Maps-Ziel \"%s\" existiert bereits", "webauthn_verification_failed": "WebAuthn-Verifizierung fehlgeschlagen: %s", + "webauthn_authenticator_failed": "Der ausgewählte Authenticator wurde nicht gefunden", + "webauthn_publickey_failed": "Zu dem ausgewählten Authenticator wurde kein Publickey hinterlegt", + "webauthn_username_failed": "Der ausgewählte Authenticator gehört zu einem anderen Konto", "unknown": "Ein unbekannter Fehler trat auf", "unknown_tfa_method": "Unbekannte TFA-Methode", "unlimited_quota_acl": "Unendliche Quota untersagt durch ACL", diff --git a/data/web/lang/lang.en-gb.json b/data/web/lang/lang.en-gb.json index f7fc0577..02db0b0d 100644 --- a/data/web/lang/lang.en-gb.json +++ b/data/web/lang/lang.en-gb.json @@ -458,6 +458,9 @@ "totp_verification_failed": "TOTP verification failed", "transport_dest_exists": "Transport destination \"%s\" exists", "webauthn_verification_failed": "WebAuthn verification failed: %s", + "webauthn_authenticator_failed": "The selected authenticator was not found", + "webauthn_publickey_failed": "No public key was stored for the selected authenticator", + "webauthn_username_failed": "The selected authenticator belongs to another account", "unknown": "An unknown error occurred", "unknown_tfa_method": "Unknown TFA method", "unlimited_quota_acl": "Unlimited quota prohibited by ACL", From 2bc663dcd573c16c4bd246646dfca15c1ac9a4dd Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Thu, 2 Feb 2023 14:55:44 +0100 Subject: [PATCH 158/170] Removed Twitter Action due to Twitter Paid API (soon). Thx Elon! --- .../tweet-trigger-publish-release.yml | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/workflows/tweet-trigger-publish-release.yml diff --git a/.github/workflows/tweet-trigger-publish-release.yml b/.github/workflows/tweet-trigger-publish-release.yml deleted file mode 100644 index 9aab121a..00000000 --- a/.github/workflows/tweet-trigger-publish-release.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: "Tweet trigger release" -on: - release: - types: [published] - -jobs: - tweet: - runs-on: ubuntu-latest - steps: - - name: "Get Release Tag" - run: | - RELEASE_TAG=$(curl https://api.github.com/repos/mailcow/mailcow-dockerized/releases/latest | jq -r '.tag_name') - - name: Tweet-trigger-publish-release - uses: mugi111/tweet-trigger-release@v1.2 - with: - consumer_key: ${{ secrets.CONSUMER_KEY }} - consumer_secret: ${{ secrets.CONSUMER_SECRET }} - access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }} - access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }} - tweet_body: 'A new mailcow update has just been released! Checkout the GitHub Page for changelog and more informations: https://github.com/mailcow/mailcow-dockerized/releases/latest' From 5dca4dac81260d92ce62f4b6c825a8c630e6c65e Mon Sep 17 00:00:00 2001 From: milkmaker Date: Sat, 4 Feb 2023 15:00:07 +0100 Subject: [PATCH 159/170] [Web] Updated lang.ru-ru.json (#5046) Co-authored-by: Aleksandr Kliushenok --- data/web/lang/lang.ru-ru.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/data/web/lang/lang.ru-ru.json b/data/web/lang/lang.ru-ru.json index 60aba927..017ffd02 100644 --- a/data/web/lang/lang.ru-ru.json +++ b/data/web/lang/lang.ru-ru.json @@ -37,7 +37,7 @@ "add_domain_only": "Только добавить домен", "add_domain_restart": "Добавить домен и перезапустить SOGo", "alias_address": "Псевдоним/ы", - "alias_address_info": "Укажите почтовые адреса разделенные запятыми или, если хотите пересылать все сообщения для домена владельцам псевдонима то: @example.com. Только домены mailcow разрешены.", + "alias_address_info": "Адрес(а) электронной почты (через запятую) или @example.com (для перехвата всех писем для домена). только домены mailcow.", "alias_domain": "Псевдоним домена", "alias_domain_info": "Действительные имена доменов, раздёленные запятыми.", "app_name": "Название приложения", @@ -335,7 +335,8 @@ "username": "Имя пользователя", "validate_license_now": "Получить лицензию на основе GUID с сервера лицензий", "verify": "Проверить", - "yes": "✓" + "yes": "✓", + "queue_unban": "разблокировать" }, "danger": { "access_denied": "Доступ запрещён, или указаны неверные данные", From dc85f4996192210cdd1ab3b2a43bccca59e7e52a Mon Sep 17 00:00:00 2001 From: Tomy Hsieh Date: Sat, 11 Feb 2023 21:49:21 +0800 Subject: [PATCH 160/170] =?UTF-8?q?=E2=9C=A8=20feat:=20Change=20FIDO2=20lo?= =?UTF-8?q?gin=20to=20independent=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/web/templates/index.twig | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/data/web/templates/index.twig b/data/web/templates/index.twig index e90a720a..45054b5f 100644 --- a/data/web/templates/index.twig +++ b/data/web/templates/index.twig @@ -38,15 +38,8 @@
    -
    -
    - - - -
    -
    + + {% if not oauth2_request %}
    '; } - item.in_use = '
    ' + + item.in_use = { + sortBy: item.percent_in_use, + value: '
    ' + '
    ' + item.percent_in_use + '%' + '
    '; + 'style="min-width:2em;width:' + item.percent_in_use + '%">' + item.percent_in_use + '%' + '
    ' + }; item.username = escapeHtml(item.username); if (Array.isArray(item.tags)){ @@ -994,10 +997,11 @@ jQuery(function($){ }, { title: lang.in_use, - data: 'in_use', + data: 'in_use.value', defaultContent: '', responsivePriority: 9, - className: 'dt-data-w100' + className: 'dt-data-w100', + orderData: 24 }, { title: lang.fname, @@ -1102,7 +1106,12 @@ jQuery(function($){ { title: "", data: 'quota.sortBy', - responsivePriority: 8, + defaultContent: '', + className: "d-none" + }, + { + title: "", + data: 'in_use.sortBy', defaultContent: '', className: "d-none" }, From 7f0dd7d0d7a52115c3b97bb8c634b4b67d0693bb Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Fri, 17 Feb 2023 12:53:31 +0100 Subject: [PATCH 168/170] [Nextcloud] Added bzip2 as required package --- helper-scripts/nextcloud.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/helper-scripts/nextcloud.sh b/helper-scripts/nextcloud.sh index 31cdb6a4..50fcbab3 100755 --- a/helper-scripts/nextcloud.sh +++ b/helper-scripts/nextcloud.sh @@ -2,9 +2,12 @@ # renovate: datasource=github-releases depName=nextcloud/server versioning=semver extractVersion=^v(?.*)$ NEXTCLOUD_VERSION=25.0.3 -for bin in curl dirmngr; do - if [[ -z $(which ${bin}) ]]; then echo "Cannot find ${bin}, exiting..."; exit 1; fi +echo -ne "Checking prerequisites..." +sleep 1 +for bin in curl dirmngr tar bzip2; do + if [[ -z $(which ${bin}) ]]; then echo -ne "\r\033[31mCannot find ${bin}, exiting...\033[0m\n"; exit 1; fi done +echo -ne "\r\033[32mFound all prerequisites! Continuing...\033[0m\n" [[ -z ${1} ]] && NC_HELP=y @@ -215,5 +218,4 @@ elif [[ ${NC_RESETPW} == "y" ]]; then read -p "Enter the username: " NC_USER done docker exec -it -u www-data $(docker ps -f name=php-fpm-mailcow -q) /web/nextcloud/occ user:resetpassword ${NC_USER} - fi From 04403aaf70b106bdffa19614b1ce25f715d4d7c0 Mon Sep 17 00:00:00 2001 From: FreddleSpl0it Date: Fri, 17 Feb 2023 13:15:44 +0100 Subject: [PATCH 169/170] [Netfilter] fix setting SNAT Rule if chain is empty --- data/Dockerfiles/netfilter/server.py | 37 +++++++++++++++++----------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/data/Dockerfiles/netfilter/server.py b/data/Dockerfiles/netfilter/server.py index 1ccc150e..0b0e2a41 100644 --- a/data/Dockerfiles/netfilter/server.py +++ b/data/Dockerfiles/netfilter/server.py @@ -359,21 +359,28 @@ def snat4(snat_target): chain = iptc.Chain(table, 'POSTROUTING') table.autocommit = False new_rule = get_snat4_rule() - for position, rule in enumerate(chain.rules): - match = all(( - new_rule.get_src() == rule.get_src(), - new_rule.get_dst() == rule.get_dst(), - new_rule.target.parameters == rule.target.parameters, - new_rule.target.name == rule.target.name - )) - if position == 0: - if not match: - logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}') - chain.insert_rule(new_rule) - else: - if match: - logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}') - chain.delete_rule(rule) + + if not chain.rules: + # if there are no rules in the chain, insert the new rule directly + logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}') + chain.insert_rule(new_rule) + else: + for position, rule in enumerate(chain.rules): + match = all(( + new_rule.get_src() == rule.get_src(), + new_rule.get_dst() == rule.get_dst(), + new_rule.target.parameters == rule.target.parameters, + new_rule.target.name == rule.target.name + )) + if position == 0: + if not match: + logInfo(f'Added POSTROUTING rule for source network {new_rule.src} to SNAT target {snat_target}') + chain.insert_rule(new_rule) + else: + if match: + logInfo(f'Remove rule for source network {new_rule.src} to SNAT target {snat_target} from POSTROUTING chain at position {position}') + chain.delete_rule(rule) + table.commit() table.autocommit = True except: From 1a4f11209a52ac38847ac9562cedf6972b25307b Mon Sep 17 00:00:00 2001 From: Niklas Meyer Date: Fri, 17 Feb 2023 13:22:23 +0100 Subject: [PATCH 170/170] Updated netfilter to 1.51 --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index d9bad657..7c6c5d6a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -425,7 +425,7 @@ services: - acme netfilter-mailcow: - image: mailcow/netfilter:1.50 + image: mailcow/netfilter:1.51 stop_grace_period: 30s depends_on: - dovecot-mailcow