Compare commits

..
Author SHA1 Message Date
FreddleSpl0it 1f8b4679cc [Nignx] Update to 1.30.3 2026-06-25 12:17:37 +02:00
9e723e942d Translations update from Weblate (#7302)
* [Web] Updated lang.fr-fr.json

Co-authored-by: Romain Ayme <ayme.romain@hotmail.fr>

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

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

---------

Co-authored-by: Romain Ayme <ayme.romain@hotmail.fr>
Co-authored-by: Matjaž Tekavec <matjaz@moj-svet.si>
2026-06-20 13:57:15 +02:00
renovate[bot]andGitHub 47e4552b44 Update actions/checkout action to v7 (#7300) 2026-06-19 08:34:02 +02:00
renovate[bot]andGitHub 7e6c36dce1 Update alpine Docker tag to v3.24 (#7280) 2026-06-11 11:35:03 +02:00
FreddleSpl0it fa154c71f3 [PHP] Rebuild Image 2026-06-11 10:29:01 +02:00
FreddleSpl0itandGitHub 83a045ed3e Merge pull request #7193 from mailcow/renovate/composer-composer-2.x
Update dependency composer/composer to v2.10.1
2026-06-11 10:07:19 +02:00
FreddleSpl0itandGitHub 064817ac70 Merge pull request #7189 from mailcow/renovate/php-pecl-mail-mailparse-3.x
Update dependency php/pecl-mail-mailparse to v3.2.0
2026-06-11 10:06:49 +02:00
FreddleSpl0itandGitHub 2bd7a24b8c Merge pull request #7212 from mailcow/mkuron-patch-mobileconfig
Escape generated password in mobileconfig
2026-06-11 10:05:17 +02:00
FreddleSpl0itandGitHub ecc2462e4c Merge pull request #7267 from goodygh/7249-update-sogo
[SOGo] Update to 5.12.9
2026-06-11 10:04:30 +02:00
FreddleSpl0itandGitHub 2d8db72d46 Merge pull request #7275 from Snafu/fix/admin-mailbox-tfa-missing
Fix force_tfa not available in mailbox template #7216
2026-06-11 10:03:39 +02:00
FreddleSpl0it 277a307fb9 [Web] Fix refresh SOGo view on mailbox deletion 2026-06-11 09:55:32 +02:00
FreddleSpl0itandGitHub d455555621 Merge pull request #7277 from ibobgunardi/bobi/mailcow-7136-sogo-active-refresh
Refresh SOGo view after mailbox activation
2026-06-11 09:53:30 +02:00
FreddleSpl0itandGitHub 9ea2ff1dff Merge pull request #7283 from mailcow/feat/update-metadata-exporter
[Rspamd] Migrate metadata_exporter to multipart formatter
2026-06-11 09:40:34 +02:00
FreddleSpl0it 24cc369d84 [Rspamd] Migrate metadata_exporter to multipart formatter 2026-06-11 09:34:27 +02:00
FreddleSpl0itandGitHub 5f5367f2f9 Merge pull request #7172 from mailcow/dragoangel-patch-4
Update RSPAMD version to 4.1.0 in Dockerfile
2026-06-11 09:04:19 +02:00
milkmakerandGitHub 03c31f825a update postscreen_access.cidr (#7269) 2026-06-09 12:20:53 +02:00
renovate[bot]andGitHub c0050e8836 Update devops-infra/action-pull-request action to v1.3.0 (#7272) 2026-06-09 12:19:09 +02:00
Dmytro AlieksieievandGitHub 15891960f7 Update RSPAMD version to 4.1.0 2026-06-09 01:19:06 +02:00
Bobby cce02e2b15 Refresh SOGo view after mailbox activation 2026-06-08 00:01:43 +07:00
Snafu 0fafda696b Fix force_tfa not available in mailbox template #7216 2026-06-07 17:03:52 +02:00
renovate[bot]andGitHub 843db5854c Update dependency composer/composer to v2.10.1
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2026-06-04 11:04:42 +00:00
goodygh 23e4e4f373 [SOGo] Update to 5.12.9 2026-05-29 01:39:38 +02:00
Michael KuronandMichael Kuron ffbc37a00c Escape generated password in mobileconfig
Escape ampersand, less than, greater than to avoid generating invalid XML.

Fixes #7171
2026-05-24 11:52:12 +02:00
renovate[bot]andGitHub 104af58628 Update dependency php/pecl-mail-mailparse to v3.2.0
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2026-05-14 18:33:03 +00:00
Dmitriy AlekseevandGitHub b0873edb6a Update RSPAMD version in Dockerfile 2026-04-06 17:28:32 +02:00
Dmitriy AlekseevandGitHub 694d751915 Update RSPAMD version in Dockerfile 2026-04-06 17:28:14 +02:00
Dmitriy AlekseevandGitHub 08278a8be9 Update RSPAMD version to 4.0.0 in Dockerfile 2026-03-30 23:51:05 +02:00
48 changed files with 909 additions and 3237 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- "watchdog-mailcow"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Setup Docker
run: |
curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh
+2 -2
View File
@@ -8,11 +8,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Run the Action
uses: devops-infra/action-pull-request@v1.2.1
uses: devops-infra/action-pull-request@v1.3.0
with:
github_token: ${{ secrets.PRTONIGHTLY_ACTION_PAT }}
title: Automatic PR to nightly from ${{ github.event.repository.updated_at}}
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
packages: write
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Generate postscreen_access.cidr
run: |
+35 -79
View File
@@ -51,47 +51,40 @@ sub sig_handler {
die "sig_handler received signal, preparing to exit...\n";
};
open my $file, '<', "/etc/sogo/sieve.creds";
my $creds = <$file>;
open my $file, '<', "/etc/sogo/sieve.creds";
my $creds = <$file>;
close $file;
my ($master_user, $master_pass) = split /:/, $creds;
# Source-based schema: join imapsync_source to read host/port/encryption/auth-type
my $sth = $dbh->prepare("SELECT
i.id,
i.user1,
i.user2,
s.host1,
s.auth_type,
i.password1,
i.exclude,
s.port1,
s.enc1,
i.delete2duplicates,
i.maxage,
i.subfolder2,
i.delete1,
i.delete2,
i.automap,
i.skipcrossduplicates,
i.maxbytespersecond,
i.custom_params,
i.subscribeall,
i.timeout1,
i.timeout2,
i.dry,
s.oauth_access_token,
s.oauth_token_expires,
s.oauth_flow,
i.source_id
FROM imapsync i JOIN imapsync_source s ON i.source_id = s.id
WHERE i.active = 1
AND s.active = 1
AND i.is_running = 0
my $sth = $dbh->prepare("SELECT id,
user1,
user2,
host1,
authmech1,
password1,
exclude,
port1,
enc1,
delete2duplicates,
maxage,
subfolder2,
delete1,
delete2,
automap,
skipcrossduplicates,
maxbytespersecond,
custom_params,
subscribeall,
timeout1,
timeout2,
dry
FROM imapsync
WHERE active = 1
AND is_running = 0
AND (
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(i.last_run) > i.mins_interval * 60
UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60
OR
i.last_run IS NULL)
ORDER BY i.last_run");
last_run IS NULL)
ORDER BY last_run");
$sth->execute();
my $row;
@@ -102,7 +95,7 @@ while ($row = $sth->fetchrow_arrayref()) {
$user1 = @$row[1];
$user2 = @$row[2];
$host1 = @$row[3];
$auth_type = @$row[4];
$authmech1 = @$row[4];
$password1 = @$row[5];
$exclude = @$row[6];
$port1 = @$row[7];
@@ -120,53 +113,16 @@ while ($row = $sth->fetchrow_arrayref()) {
$timeout1 = @$row[19];
$timeout2 = @$row[20];
$dry = @$row[21];
$oauth_access_token = @$row[22];
$oauth_token_expires = @$row[23];
$oauth_flow = @$row[24];
$source_id = @$row[25];
if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; }
my $template = $run_dir . '/imapsync.XXXXXXX';
my $passfile1 = File::Temp->new(TEMPLATE => $template);
my $passfile2 = File::Temp->new(TEMPLATE => $template);
my $tokenfile1;
binmode( $passfile1, ":utf8" );
# Auth-mode-specific argument set
my @auth_args;
if (defined($auth_type) && $auth_type eq 'XOAUTH2') {
# authorization_code sources use a per-user token (keyed by source + user1),
# not the shared source-level token used by client_credentials.
if (defined($oauth_flow) && $oauth_flow eq 'authorization_code') {
my $tsth = $dbh->prepare("SELECT access_token, token_expires FROM imapsync_source_oauth_token WHERE source_id=? AND username=?");
$tsth->execute($source_id, $user1);
my $trow = $tsth->fetchrow_arrayref();
$oauth_access_token = $trow ? @$trow[0] : undef;
$oauth_token_expires = $trow ? @$trow[1] : undef;
}
# Skip sync if cached token is missing or about to expire (<2 min)
if (!defined($oauth_access_token) || $oauth_access_token eq ''
|| !defined($oauth_token_expires) || $oauth_token_expires - time() < 120) {
my $upd = $dbh->prepare("UPDATE imapsync SET returned_text=?, success=0, exit_status='OAUTH_TOKEN_NOT_READY', last_run=NOW(), is_running=0 WHERE id=?");
$upd->execute("OAuth access token for the configured source is missing or expires too soon; refresh cron will retry.", $id);
next;
}
$tokenfile1 = File::Temp->new(TEMPLATE => $template);
binmode($tokenfile1, ":utf8");
print $tokenfile1 "$oauth_access_token\n";
@auth_args = ("--authmech1", "XOAUTH2",
"--oauthaccesstoken1", $tokenfile1->filename);
} else {
print $passfile1 "$password1\n";
@auth_args = ("--passfile1", $passfile1->filename);
# imapsync auto-picks PLAIN; only emit --authmech1 for LOGIN/CRAM-MD5
if (defined($auth_type) && $auth_type ne 'PLAIN') {
push @auth_args, "--authmech1", $auth_type;
}
}
print $passfile1 "$password1\n";
print $passfile2 trim($master_pass) . "\n";
my @custom_params_a = qqw($custom_params);
@@ -191,7 +147,7 @@ while ($row = $sth->fetchrow_arrayref()) {
(!defined($enc1) ? () : ($enc1)),
"--host1", $host1,
"--user1", $user1,
@auth_args,
"--passfile1", $passfile1->filename,
"--port1", $port1,
"--host2", "localhost",
"--user2", $user2 . '*' . trim($master_user),
+1 -1
View File
@@ -1,4 +1,4 @@
FROM nginx:1.30.2-alpine
FROM nginx:1.30.3-alpine
LABEL maintainer "The Infrastructure Company GmbH <info@servercow.de>"
ENV PIP_BREAK_SYSTEM_PACKAGES=1
+2 -2
View File
@@ -7,13 +7,13 @@ ARG APCU_PECL_VERSION=5.1.28
# renovate: datasource=github-tags depName=Imagick/imagick versioning=semver-coerced extractVersion=(?<version>.*)$
ARG IMAGICK_PECL_VERSION=3.8.1
# renovate: datasource=github-tags depName=php/pecl-mail-mailparse versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MAILPARSE_PECL_VERSION=3.1.9
ARG MAILPARSE_PECL_VERSION=3.2.0
# renovate: datasource=github-tags depName=php-memcached-dev/php-memcached versioning=semver-coerced extractVersion=^v(?<version>.*)$
ARG MEMCACHED_PECL_VERSION=3.4.0
# renovate: datasource=github-tags depName=phpredis/phpredis versioning=semver-coerced extractVersion=(?<version>.*)$
ARG REDIS_PECL_VERSION=6.3.0
# renovate: datasource=github-tags depName=composer/composer versioning=semver-coerced extractVersion=(?<version>.*)$
ARG COMPOSER_VERSION=2.9.5
ARG COMPOSER_VERSION=2.10.1
RUN apk add -U --no-cache autoconf \
aspell-dev \
+1 -1
View File
@@ -2,7 +2,7 @@ FROM debian:trixie-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_3.14.3-1~236eb65
ARG RSPAMD_VER=rspamd_4.1.0-1~e2b0b18
ARG CODENAME=trixie
ENV LC_ALL=C
+3 -3
View File
@@ -1,6 +1,6 @@
# SOGo built from source to enable security patch application
# Repository: https://github.com/Alinto/sogo
# Version: SOGo-5.12.8
# Version: SOGo-5.12.9
#
# Applied security patches:
# -
@@ -12,8 +12,8 @@ FROM debian:bookworm
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive
ARG SOGO_VERSION=SOGo-5.12.8
ARG SOPE_VERSION=SOPE-5.12.8
ARG SOGO_VERSION=SOGo-5.12.9
ARG SOPE_VERSION=SOPE-5.12.9
# Security patches to apply (space-separated commit hashes)
ARG SOGO_SECURITY_PATCHES=""
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
@@ -1,100 +0,0 @@
<?php
require_once(__DIR__ . '/../web/inc/vars.inc.php');
if (file_exists(__DIR__ . '/../web/inc/vars.local.inc.php')) {
include_once(__DIR__ . '/../web/inc/vars.local.inc.php');
}
require_once __DIR__ . '/../web/inc/lib/vendor/autoload.php';
$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,
];
try {
$pdo = new PDO($dsn, $database_user, $database_pass, $opt);
} catch (PDOException $e) {
fwrite(STDERR, "DB connect failed: " . $e->getMessage() . "\n");
exit(1);
}
$redis = new Redis();
try {
if (!empty(getenv('REDIS_SLAVEOF_IP'))) {
$redis->connect(getenv('REDIS_SLAVEOF_IP'), getenv('REDIS_SLAVEOF_PORT'));
} else {
$redis->connect('redis-mailcow', 6379);
}
$redis->auth(getenv("REDISPASS"));
} catch (Exception $e) {
fwrite(STDERR, "Redis connect failed: " . $e->getMessage() . "\n");
exit(1);
}
function logMsg($priority, $message, $task = "imapsync OAuth refresh") {
global $redis;
$redis->lPush('CRON_LOG', json_encode(array(
"time" => time(),
"priority" => $priority,
"task" => $task,
"message" => $message,
)));
}
require_once __DIR__ . '/../web/inc/functions.inc.php';
require_once __DIR__ . '/../web/inc/functions.auth.inc.php';
require_once __DIR__ . '/../web/inc/sessions.inc.php';
require_once __DIR__ . '/../web/inc/functions.mailbox.inc.php';
require_once __DIR__ . '/../web/inc/functions.syncjob.inc.php';
// File-lock to prevent concurrent runs
$lock_file = '/tmp/imapsync-oauth-refresh.lock';
if (file_exists($lock_file)) {
$pid = (int)trim(@file_get_contents($lock_file));
if ($pid > 0 && posix_kill($pid, 0)) {
logMsg("info", "Previous refresh still running (pid=$pid), exiting");
exit(0);
}
@unlink($lock_file);
}
file_put_contents($lock_file, getmypid());
try {
// 1) client_credentials sources: refresh the shared source-level token (expires < 5 min or none)
$stmt = $pdo->prepare("SELECT `id`, `name` FROM `imapsync_source`
WHERE `active` = 1 AND `auth_type` = 'XOAUTH2' AND `oauth_flow` = 'client_credentials'
AND (`oauth_token_expires` IS NULL OR `oauth_token_expires` < :soon)");
$stmt->execute(array(':soon' => time() + 300));
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $src) {
$ok = imapsync_source_refresh_token_internal($src['id']);
if ($ok) {
logMsg("info", "Refreshed token for source '" . $src['name'] . "' (id=" . $src['id'] . ")");
} else {
$e = $pdo->prepare("SELECT `oauth_last_refresh_error` FROM `imapsync_source` WHERE `id` = :id");
$e->execute(array(':id' => $src['id']));
logMsg("warning", "Refresh failed for source '" . $src['name'] . "' (id=" . $src['id'] . "): " . $e->fetchColumn());
}
}
// 2) authorization_code sources: refresh each per-user token via its stored refresh_token
$stmt = $pdo->prepare("SELECT t.`source_id`, t.`username`, s.`name`
FROM `imapsync_source_oauth_token` t
JOIN `imapsync_source` s ON s.`id` = t.`source_id`
WHERE s.`active` = 1 AND s.`auth_type` = 'XOAUTH2' AND s.`oauth_flow` = 'authorization_code'
AND t.`refresh_token` <> ''
AND (t.`token_expires` IS NULL OR t.`token_expires` < :soon)");
$stmt->execute(array(':soon' => time() + 300));
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $tok) {
$ok = imapsync_source_refresh_user_token_internal($tok['source_id'], $tok['username']);
if ($ok) {
logMsg("info", "Refreshed user token '" . $tok['username'] . "' on source '" . $tok['name'] . "' (id=" . $tok['source_id'] . ")");
} else {
$e = $pdo->prepare("SELECT `last_refresh_error` FROM `imapsync_source_oauth_token` WHERE `source_id` = :sid AND `username` = :user");
$e->execute(array(':sid' => $tok['source_id'], ':user' => $tok['username']));
logMsg("warning", "User-token refresh failed for '" . $tok['username'] . "' on source '" . $tok['name'] . "' (id=" . $tok['source_id'] . "): " . $e->fetchColumn());
}
}
} finally {
@unlink($lock_file);
}
+5 -68
View File
@@ -1,6 +1,6 @@
# Whitelist generated by Postwhite v3.4 on Fri May 1 00:43:37 UTC 2026
# Whitelist generated by Postwhite v3.4 on Mon Jun 1 00:52:17 UTC 2026
# https://github.com/stevejenkins/postwhite/
# 2249 total rules
# 2186 total rules
2a00:1450:4000::/36 permit
2a00:1450:4864::/56 permit
2a01:111:f400::/48 permit
@@ -56,14 +56,8 @@
8.25.194.0/23 permit
8.25.196.0/23 permit
8.36.116.0/24 permit
8.39.54.0/23 permit
8.39.54.250/31 permit
8.39.144.0/24 permit
8.40.222.0/23 permit
8.40.222.250/31 permit
12.130.86.238 permit
13.107.213.40 permit
13.107.246.40 permit
13.108.16.0/20 permit
13.110.208.0/21 permit
13.110.209.0/24 permit
@@ -73,7 +67,6 @@
13.111.191.0/24 permit
13.216.7.111 permit
13.216.54.180 permit
13.247.164.219 permit
15.200.21.50 permit
15.200.44.248 permit
15.200.201.185 permit
@@ -177,7 +170,6 @@
34.215.104.144 permit
34.218.115.239 permit
34.225.212.172 permit
34.241.242.183 permit
35.83.148.184 permit
35.155.198.111 permit
35.158.23.94 permit
@@ -201,7 +193,6 @@
40.233.64.216 permit
40.233.83.78 permit
40.233.88.28 permit
43.239.212.33 permit
44.206.138.57 permit
44.210.169.44 permit
44.217.45.156 permit
@@ -284,7 +275,6 @@
51.83.17.38 permit
52.1.14.157 permit
52.5.230.59 permit
52.6.74.205 permit
52.12.53.23 permit
52.13.214.179 permit
52.26.1.71 permit
@@ -341,7 +331,6 @@
54.244.54.130 permit
54.244.242.0/24 permit
54.255.61.23 permit
56.124.6.228 permit
57.103.64.0/18 permit
57.129.93.249 permit
62.13.128.0/24 permit
@@ -408,7 +397,6 @@
65.110.161.77 permit
65.123.29.213 permit
65.123.29.220 permit
65.154.166.0/24 permit
65.212.180.36 permit
66.102.0.0/20 permit
66.119.150.192/26 permit
@@ -1222,9 +1210,6 @@
99.78.197.208/28 permit
103.9.96.0/22 permit
103.28.42.0/24 permit
103.84.217.15 permit
103.84.217.238 permit
103.89.75.238 permit
103.151.192.0/23 permit
103.168.172.128/27 permit
103.237.104.0/22 permit
@@ -1385,9 +1370,6 @@
117.120.16.0/21 permit
119.42.242.52/31 permit
119.42.242.156 permit
121.244.91.48 permit
121.244.91.52 permit
122.15.156.182 permit
123.126.78.64/29 permit
124.108.96.24/31 permit
124.108.96.28/31 permit
@@ -1470,21 +1452,7 @@
134.170.141.64/26 permit
134.170.143.0/24 permit
134.170.174.0/24 permit
135.84.80.0/24 permit
135.84.81.0/24 permit
135.84.82.0/24 permit
135.84.83.0/24 permit
135.84.216.0/22 permit
136.143.160.0/24 permit
136.143.161.0/24 permit
136.143.162.0/24 permit
136.143.176.0/24 permit
136.143.177.0/24 permit
136.143.178.49 permit
136.143.182.0/23 permit
136.143.184.0/24 permit
136.143.188.0/24 permit
136.143.190.0/23 permit
136.146.128.0/20 permit
136.147.128.0/20 permit
136.147.135.0/24 permit
@@ -1504,7 +1472,6 @@
139.138.46.219 permit
139.138.57.55 permit
139.138.58.119 permit
139.167.79.86 permit
139.177.108.0/25 permit
139.180.17.0/24 permit
140.238.148.191 permit
@@ -1554,12 +1521,14 @@
147.243.128.26 permit
148.105.0.0/16 permit
148.105.8.0/21 permit
148.116.32.128/25 permit
149.72.0.0/16 permit
149.72.234.184 permit
149.72.248.236 permit
149.97.173.180 permit
149.118.160.128/25 permit
150.136.21.199 permit
150.171.109.118 permit
150.230.98.160 permit
151.145.38.14 permit
152.67.105.195 permit
@@ -1636,11 +1605,6 @@
164.152.25.241 permit
164.177.132.168/30 permit
165.1.100.0/25 permit
165.173.128.0/24 permit
165.173.180.1 permit
165.173.180.250/31 permit
165.173.182.250/31 permit
165.173.189.205 permit
166.78.68.0/22 permit
166.78.68.221 permit
166.78.69.169 permit
@@ -1673,19 +1637,6 @@
168.245.12.252 permit
168.245.46.9 permit
168.245.127.231 permit
169.148.129.0/24 permit
169.148.131.0/24 permit
169.148.138.0/24 permit
169.148.142.10 permit
169.148.142.33 permit
169.148.144.0/25 permit
169.148.144.10 permit
169.148.146.0/23 permit
169.148.174.10 permit
169.148.175.3 permit
169.148.179.3 permit
169.148.188.0/24 permit
169.148.188.182 permit
170.9.232.254 permit
170.10.128.0/24 permit
170.10.129.0/24 permit
@@ -1891,16 +1842,7 @@
199.16.156.0/22 permit
199.33.145.1 permit
199.33.145.32 permit
199.34.22.36 permit
199.59.148.0/22 permit
199.67.80.2 permit
199.67.80.20 permit
199.67.82.2 permit
199.67.82.20 permit
199.67.84.0/24 permit
199.67.86.0/24 permit
199.67.88.0/24 permit
199.67.90.0/24 permit
199.101.161.130 permit
199.101.162.0/25 permit
199.122.120.0/21 permit
@@ -1956,8 +1898,6 @@
204.92.114.187 permit
204.92.114.203 permit
204.92.114.204/31 permit
204.141.32.0/23 permit
204.141.42.0/23 permit
204.216.164.202 permit
204.220.160.0/21 permit
204.220.168.0/21 permit
@@ -2231,10 +2171,7 @@
2001:748:400:3301::4 permit
2404:6800:4000::/36 permit
2404:6800:4864::/56 permit
2603:1061:14:72::1 permit
2607:13c0:0001:0000:0000:0000:0000:7000/116 permit
2607:13c0:0002:0000:0000:0000:0000:1000/116 permit
2607:13c0:0004:0000:0000:0000:0000:0000/116 permit
2603:1061:14:40::1 permit
2607:f8b0:4000::/36 permit
2607:f8b0:4864::/56 permit
2620:109:c003:104::/64 permit
@@ -3,8 +3,7 @@ rules {
backend = "http";
url = "http://nginx:9081/pipe.php";
selector = "reject_no_global_bl";
formatter = "default";
meta_headers = true;
formatter = "multipart";
}
RLINFO {
backend = "http";
@@ -16,8 +15,7 @@ rules {
backend = "http";
url = "http://nginx:9081/pushover.php";
selector = "mailcow_rcpt";
formatter = "json";
meta_headers = true;
formatter = "multipart";
}
}
+27 -32
View File
@@ -32,47 +32,42 @@ function parse_email($email) {
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
// rspamd metadata_exporter (multipart formatter):
// - $_POST['metadata'] JSON with the rspamd metadata
// - $_FILES['message'] raw RFC822 message
if (empty($_POST['metadata']) || !isset($_FILES['message']) || $_FILES['message']['error'] !== UPLOAD_ERR_OK) {
error_log("QUARANTINE: missing multipart parts from rspamd" . PHP_EOL);
http_response_code(400);
exit;
}
$raw_data_content = file_get_contents('php://input');
$meta = json_decode($_POST['metadata'], true);
if (!is_array($meta)) {
error_log("QUARANTINE: cannot decode metadata JSON" . PHP_EOL);
http_response_code(400);
exit;
}
$raw_data_content = file_get_contents($_FILES['message']['tmp_name']);
$raw_data = mb_convert_encoding($raw_data_content, 'HTML-ENTITIES', "UTF-8");
$headers = getallheaders();
$raw_size = (int)$_FILES['message']['size'];
$qid = $headers['X-Rspamd-Qid'];
$fuzzy = $headers['X-Rspamd-Fuzzy'];
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
$score = $headers['X-Rspamd-Score'];
$rcpts = $headers['X-Rspamd-Rcpt'];
$user = $headers['X-Rspamd-User'];
$ip = $headers['X-Rspamd-Ip'];
$action = $headers['X-Rspamd-Action'];
$sender = $headers['X-Rspamd-From'];
$symbols = $headers['X-Rspamd-Symbols'];
$raw_size = (int)$_SERVER['CONTENT_LENGTH'];
$qid = $meta['qid'] ?? 'unknown';
$subject = iconv_mime_decode($meta['subject'] ?? '');
$score = $meta['score'] ?? 0;
$rcpts = $meta['rcpt'] ?? array();
$user = $meta['user'] ?? 'unknown';
$ip = $meta['ip'] ?? 'unknown';
$action = $meta['action'] ?? 'no action';
$sender = $meta['from'] ?? '';
$symbols = json_encode($meta['symbols'] ?? array());
$fuzzy = json_encode(is_array($meta['fuzzy'] ?? null) ? $meta['fuzzy'] : array());
if (empty($sender)) {
error_log("QUARANTINE: Unknown sender, assuming empty-env-from@localhost" . PHP_EOL);
$sender = 'empty-env-from@localhost';
}
if ($fuzzy == 'unknown') {
$fuzzy = '[]';
}
try {
$max_size = (int)$redis->Get('Q_MAX_SIZE');
if (($max_size * 1048576) < $raw_size) {
@@ -94,7 +89,7 @@ catch (RedisException $e) {
$rcpt_final_mailboxes = array();
// Loop through all rcpts
foreach (json_decode($rcpts, true) as $rcpt) {
foreach ($rcpts as $rcpt) {
// Remove tag
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
+22 -26
View File
@@ -32,50 +32,46 @@ function parse_email($email) {
$a = strrpos($email, '@');
return array('local' => substr($email, 0, $a), 'domain' => substr(substr($email, $a), 1));
}
if (!function_exists('getallheaders')) {
function getallheaders() {
if (!is_array($_SERVER)) {
return array();
}
$headers = array();
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}
// rspamd metadata_exporter (multipart formatter): metadata JSON arrives as $_POST['metadata'].
if (empty($_POST['metadata'])) {
error_log("NOTIFY: missing metadata part from rspamd" . PHP_EOL);
http_response_code(400);
exit;
}
$headers = getallheaders();
$json_body = json_decode(file_get_contents('php://input'));
$meta = json_decode($_POST['metadata'], true);
if (!is_array($meta)) {
error_log("NOTIFY: cannot decode metadata JSON" . PHP_EOL);
http_response_code(400);
exit;
}
$qid = $headers['X-Rspamd-Qid'];
$rcpts = $headers['X-Rspamd-Rcpt'];
$sender = $headers['X-Rspamd-From'];
$ip = $headers['X-Rspamd-Ip'];
$subject = iconv_mime_decode($headers['X-Rspamd-Subject']);
$messageid= $json_body->message_id;
$qid = $meta['qid'] ?? 'unknown';
$rcpts = $meta['rcpt'] ?? array();
$sender = $meta['from'] ?? '';
$ip = $meta['ip'] ?? 'unknown';
$subject = iconv_mime_decode($meta['subject'] ?? '');
$messageid= $meta['message_id'] ?? '';
$priority = 0;
$symbols_array = json_decode($headers['X-Rspamd-Symbols'], true);
$symbols_array = $meta['symbols'] ?? array();
if (is_array($symbols_array)) {
foreach ($symbols_array as $symbol) {
if ($symbol['name'] == 'HAS_X_PRIO_ONE') {
if (($symbol['name'] ?? null) == 'HAS_X_PRIO_ONE') {
$priority = 1;
break;
}
}
}
$sender_address = $json_body->header_from[0];
$sender_address = $meta['header_from'][0] ?? '';
$sender_name = '-';
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $sender_address, $matches)) {
$sender_address = $matches['address'];
$sender_name = trim($matches['name'], '"\' ');
}
$to_address = $json_body->header_to[0];
$to_address = $meta['header_to'][0] ?? '';
$to_name = '-';
if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
$to_address = $matches['address'];
@@ -85,7 +81,7 @@ if (preg_match('/(?<name>.*?)<(?<address>.*?)>/i', $to_address, $matches)) {
$rcpt_final_mailboxes = array();
// Loop through all rcpts
foreach (json_decode($rcpts, true) as $rcpt) {
foreach ($rcpts as $rcpt) {
// Remove tag
$rcpt = preg_replace('/^(.*?)\+.*(@.*)$/', '$1$2', $rcpt);
+12 -403
View File
@@ -1072,6 +1072,7 @@ paths:
password2: "*"
quota: "3072"
force_pw_update: "1"
force_tfa: "1"
tls_enforce_in: "1"
tls_enforce_out: "1"
tags: ["tag1", "tag2"]
@@ -1118,6 +1119,7 @@ paths:
password2: atedismonsin
quota: "3072"
force_pw_update: "1"
force_tfa: "1"
tls_enforce_in: "1"
tls_enforce_out: "1"
tags: ["tag1", "tag2"]
@@ -1151,6 +1153,9 @@ paths:
force_pw_update:
description: forces the user to update its password on first login
type: boolean
force_tfa:
description: force 2FA enrollment at login
type: boolean
tls_enforce_in:
description: force inbound email tls encryption
type: boolean
@@ -1593,126 +1598,6 @@ paths:
description: enables or disables the sync job
type: boolean
type: object
/api/v1/add/syncjob_source:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- log:
- syncjob
- add
- source
- name: Example import
description: ""
host1: imap.example.org
port1: 993
enc1: SSL
auth_type: PLAIN
active: 1
is_global: 0
msg:
- imapsync_source_added
- Example import
type: success
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Sync jobs
description: >-
Create a reusable IMAP sync source. A sync job binds to a source by id;
the source carries host/port/encryption and either static credentials
(PLAIN/LOGIN/CRAM-MD5 — supplied per-job) or an OAuth2 token endpoint
(XOAUTH2 — token cached on the source row).
operationId: Create sync job source
summary: Create sync job source
requestBody:
content:
application/json:
schema:
example:
name: Example import
description: ""
host1: imap.example.org
port1: "993"
enc1: SSL
auth_type: PLAIN
active: "1"
is_global: "0"
properties:
name:
description: source name, unique per owner
type: string
description:
description: free text shown in the UI
type: string
host1:
description: remote IMAP host
type: string
port1:
description: remote IMAP port (1-65535)
type: integer
enc1:
description: TLS / SSL / PLAIN
type: string
enum:
- TLS
- SSL
- PLAIN
auth_type:
description: authentication mechanism
type: string
enum:
- PLAIN
- LOGIN
- CRAM-MD5
- XOAUTH2
oauth_token_endpoint:
description: required when auth_type is XOAUTH2 (token URL)
type: string
oauth_client_id:
description: required when auth_type is XOAUTH2
type: string
oauth_client_secret:
description: required when auth_type is XOAUTH2
type: string
oauth_scope:
description: required when auth_type is XOAUTH2
type: string
oauth_extra_params:
description: optional JSON object merged into the token request body
type: string
active:
description: enables or disables the source
type: boolean
is_global:
description: admin-only — make the source available to everyone
type: boolean
owner:
description: admin-only — assign ownership to a specific username
type: string
type: object
/api/v1/add/tls-policy-map:
post:
responses:
@@ -2885,62 +2770,6 @@ paths:
type: object
type: object
summary: Delete sync job
/api/v1/delete/syncjob_source:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- log:
- syncjob
- delete
- source
- id:
- "3"
msg:
- imapsync_source_deleted
- Example import
type: success
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Sync jobs
description: >-
Delete one or more sync job sources by id. Sources still referenced by
a sync job cannot be deleted (FK ON DELETE RESTRICT) — delete or
repoint the dependent jobs first.
operationId: Delete sync job source
summary: Delete sync job source
requestBody:
content:
application/json:
schema:
example:
- "3"
items:
type: string
type: array
/api/v1/delete/tls-policy-map:
post:
responses:
@@ -3590,6 +3419,7 @@ paths:
- mailbox
- active: "1"
force_pw_update: "0"
force_tfa: "0"
name: Full name
password: "*"
password2: "*"
@@ -3640,6 +3470,7 @@ paths:
attr:
active: "1"
force_pw_update: "0"
force_tfa: "0"
name: Full name
authsource: mailcow
password: ""
@@ -3663,6 +3494,9 @@ paths:
force_pw_update:
description: force user to change password on next login
type: boolean
force_tfa:
description: force 2FA enrollment at login
type: boolean
name:
description: Full name of the mailbox user
type: string
@@ -4107,177 +3941,6 @@ paths:
type: object
type: object
summary: Update sync job
/api/v1/edit/syncjob_source:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- log:
- syncjob
- edit
- source
- id:
- "3"
name: Example import
host1: imap.example.org
port1: 993
msg:
- imapsync_source_modified
- Example import
type: success
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Sync jobs
description: >-
Update one or more sync job sources. Fields not provided are left
unchanged. For XOAUTH2 sources, an empty `oauth_client_secret`
preserves the stored value.
operationId: Update sync job source
summary: Update sync job source
requestBody:
content:
application/json:
schema:
example:
items:
- "3"
attr:
name: Example import
description: ""
host1: imap.example.org
port1: "993"
enc1: SSL
auth_type: PLAIN
active: "1"
properties:
attr:
properties:
name:
type: string
description:
type: string
host1:
type: string
port1:
type: integer
enc1:
type: string
enum:
- TLS
- SSL
- PLAIN
auth_type:
type: string
enum:
- PLAIN
- LOGIN
- CRAM-MD5
- XOAUTH2
oauth_token_endpoint:
type: string
oauth_client_id:
type: string
oauth_client_secret:
description: empty keeps the previously stored secret
type: string
oauth_scope:
type: string
oauth_extra_params:
type: string
active:
type: boolean
type: object
items:
description: list of source ids to update
type: array
items:
type: string
type: object
/api/v1/edit/syncjob_source/refresh_token:
post:
responses:
"401":
$ref: "#/components/responses/Unauthorized"
"200":
content:
application/json:
examples:
response:
value:
- log:
- syncjob
- edit
- refresh_token
- id:
- "3"
msg:
- imapsync_source_token_refreshed
- Example import
type: success
schema:
properties:
log:
description: contains request object
items: {}
type: array
msg:
items: {}
type: array
type:
enum:
- success
- danger
- error
type: string
type: object
description: OK
headers: {}
tags:
- Sync jobs
description: >-
Force an OAuth2 access-token refresh for a XOAUTH2 source. Performs a
Client-Credentials grant against the source's `oauth_token_endpoint`
and caches the new access token + expiry on the source row. On failure,
the error is recorded in `oauth_last_refresh_error`.
operationId: Refresh sync job source token
summary: Refresh sync job source token
requestBody:
content:
application/json:
schema:
example:
items:
- "3"
properties:
items:
description: list of source ids to refresh
type: array
items:
type: string
type: object
/api/v1/edit/user-acl:
post:
responses:
@@ -5228,6 +4891,7 @@ paths:
- active: "1"
attributes:
force_pw_update: "0"
force_tfa: "0"
mailbox_format: "maildir:"
quarantine_notification: never
sogo_access: "1"
@@ -5976,62 +5640,6 @@ paths:
description: You can list all syn jobs existing in system.
operationId: Get sync jobs
summary: Get sync jobs
"/api/v1/get/syncjob_source/{id}":
get:
parameters:
- description: source id, or `all` to list every source visible to the session
example: all
in: path
name: id
required: true
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:
- id: 3
name: Example import
description: ""
owner: null
host1: imap.example.org
port1: 993
enc1: SSL
auth_type: PLAIN
oauth_token_endpoint: null
oauth_client_id: null
oauth_client_secret: ""
oauth_scope: null
oauth_extra_params: null
oauth_access_token: ""
oauth_token_expires: null
oauth_last_refresh_error: null
active: 1
created: "2026-06-11 09:00:00"
modified: "2026-06-11 09:00:00"
description: OK
headers: {}
tags:
- Sync jobs
description: >-
Returns a single sync job source by id, or all sources visible to the
caller when `id` is `all`. Secrets (`oauth_client_secret`,
`oauth_access_token`) are blanked unless requested with internal
flags — they are never returned over the JSON API.
operationId: Get sync job sources
summary: Get sync job sources
"/api/v1/get/tls-policy-map/{id}":
get:
parameters:
@@ -6208,6 +5816,7 @@ paths:
- active: "1"
attributes:
force_pw_update: "0"
force_tfa: "0"
mailbox_format: "maildir:"
quarantine_notification: never
sogo_access: "1"
+1 -7
View File
@@ -204,15 +204,9 @@ if (isset($_SESSION['mailcow_cc_role'])) {
if (isset($_GET['syncjob']) &&
is_numeric($_GET['syncjob'])) {
$id = $_GET["syncjob"];
$result = syncjob('get', 'job', $id);
$result = mailbox('get', 'syncjob_details', $id);
$template = 'edit/syncjob.twig';
}
elseif (isset($_GET['syncjob_source']) &&
is_numeric($_GET['syncjob_source'])) {
$id = $_GET["syncjob_source"];
$result = syncjob('get', 'source', array('id' => $id, 'with_secret' => true));
$template = 'edit/imapsync_source.twig';
}
elseif (isset($_GET['filter']) &&
is_numeric($_GET['filter'])) {
$id = $_GET["filter"];
+1 -1
View File
@@ -6,7 +6,7 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
}
if (isset($_GET['id']) && is_numeric($_GET['id'])) {
if ($details = syncjob('get', 'job', intval($_GET['id']))) {
if ($details = mailbox('get', 'syncjob_details', intval($_GET['id']))) {
echo (empty($details['log'])) ? '-' : $details['log'];
}
}
+2 -3
View File
@@ -33,11 +33,11 @@ if (array_key_exists('pending_tfa_methods', $_SESSION)) {
if (isset($pending_tfa_authmechs['webauthn'])) {
$pending_tfa_authmechs['webauthn'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
if (!isset($pending_tfa_authmechs['webauthn'])
&& isset($pending_tfa_authmechs['yubi_otp'])) {
$pending_tfa_authmechs['yubi_otp'] = true;
}
if (!isset($pending_tfa_authmechs['webauthn'])
if (!isset($pending_tfa_authmechs['webauthn'])
&& !isset($pending_tfa_authmechs['yubi_otp'])
&& isset($pending_tfa_authmechs['totp'])) {
$pending_tfa_authmechs['totp'] = true;
@@ -72,7 +72,6 @@ $globalVariables = [
'lang_fido2' => json_encode($lang['fido2']),
'lang_success' => json_encode($lang['success']),
'lang_danger' => json_encode($lang['danger']),
'lang_syncjobs' => json_encode($lang['syncjobs']),
'docker_timeout' => $DOCKER_TIMEOUT,
'session_lifetime' => (int)$SESSION_LIFETIME,
'csrf_token' => $_SESSION['CSRF']['TOKEN'],
+485 -2
View File
@@ -299,6 +299,192 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
'msg' => array('mailbox_modified', $username)
);
break;
case 'syncjob':
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (isset($_data['username']) && filter_var($_data['username'], FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data['username'])) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
else {
$username = $_data['username'];
}
}
elseif ($_SESSION['mailcow_cc_role'] == "user") {
$username = $_SESSION['mailcow_cc_username'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'no_user_defined'
);
return false;
}
$active = intval($_data['active']);
$subscribeall = intval($_data['subscribeall']);
$delete2duplicates = intval($_data['delete2duplicates']);
$delete1 = intval($_data['delete1']);
$delete2 = intval($_data['delete2']);
$timeout1 = intval($_data['timeout1']);
$timeout2 = intval($_data['timeout2']);
$skipcrossduplicates = intval($_data['skipcrossduplicates']);
$automap = intval($_data['automap']);
$dry = intval($_data['dry']);
$port1 = $_data['port1'];
$host1 = strtolower($_data['host1']);
$password1 = $_data['password1'];
$exclude = $_data['exclude'];
$maxage = $_data['maxage'];
$maxbytespersecond = $_data['maxbytespersecond'];
$subfolder2 = $_data['subfolder2'];
$user1 = $_data['user1'];
$mins_interval = $_data['mins_interval'];
$enc1 = $_data['enc1'];
$custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']);
// validate custom params
foreach (explode('-', $custom_params) as $param){
if(empty($param)) continue;
// extract option
if (str_contains($param, '=')) $param = explode('=', $param)[0];
else $param = rtrim($param, ' ');
// remove first char if first char is -
if ($param[0] == '-') $param = ltrim($param, $param[0]);
if (str_contains($param, ' ')) {
// bad char
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'bad character SPACE'
);
return false;
}
// check if param is whitelisted
if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
// bad option
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'bad option '. $param
);
return false;
}
}
if (empty($subfolder2)) {
$subfolder2 = "";
}
if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$maxage = "0";
}
if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$timeout1 = "600";
}
if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$timeout2 = "600";
}
if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) {
$maxbytespersecond = "0";
}
if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
// if (!is_valid_domain_name($host1)) {
// $_SESSION['return'][] = array(
// 'type' => 'danger',
// 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
// 'msg' => 'access_denied'
// );
// return false;
// }
if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
if (@preg_match("/" . $exclude . "/", null) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
$stmt = $pdo->prepare("SELECT '1' FROM `imapsync`
WHERE `user2` = :user2 AND `user1` = :user1 AND `host1` = :host1");
$stmt->execute(array(':user1' => $user1, ':user2' => $username, ':host1' => $host1));
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('object_exists', htmlspecialchars($host1 . ' / ' . $user1))
);
return false;
}
$stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `delete2`, `timeout1`, `timeout2`, `automap`, `skipcrossduplicates`, `maxbytespersecond`, `subscribeall`, `dry`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `custom_params`, `active`)
VALUES (:user2, :exclude, :delete1, :delete2, :timeout1, :timeout2, :automap, :skipcrossduplicates, :maxbytespersecond, :subscribeall, :dry, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :custom_params, :active)");
$stmt->execute(array(
':user2' => $username,
':custom_params' => $custom_params,
':exclude' => $exclude,
':maxage' => $maxage,
':delete1' => $delete1,
':delete2' => $delete2,
':timeout1' => $timeout1,
':timeout2' => $timeout2,
':automap' => $automap,
':skipcrossduplicates' => $skipcrossduplicates,
':maxbytespersecond' => $maxbytespersecond,
':subscribeall' => $subscribeall,
':dry' => $dry,
':subfolder2' => $subfolder2,
':host1' => $host1,
':authmech1' => 'PLAIN',
':user1' => $user1,
':password1' => $password1,
':mins_interval' => $mins_interval,
':port1' => $port1,
':enc1' => $enc1,
':delete2duplicates' => $delete2duplicates,
':active' => $active,
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
break;
case 'domain':
if ($_SESSION['mailcow_cc_role'] != "admin") {
$_SESSION['return'][] = array(
@@ -2079,6 +2265,202 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
);
}
break;
case 'syncjob':
if (!is_array($_data['id'])) {
$ids = array();
$ids[] = $_data['id'];
}
else {
$ids = $_data['id'];
}
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($ids as $id) {
$is_now = mailbox('get', 'syncjob_details', $id, array('with_password'));
if (!empty($is_now)) {
$username = $is_now['user2'];
$user1 = (!empty($_data['user1'])) ? $_data['user1'] : $is_now['user1'];
$active = (isset($_data['active'])) ? intval($_data['active']) : $is_now['active'];
$last_run = (isset($_data['last_run'])) ? NULL : $is_now['last_run'];
$success = (isset($_data['success'])) ? NULL : $is_now['success'];
$delete2duplicates = (isset($_data['delete2duplicates'])) ? intval($_data['delete2duplicates']) : $is_now['delete2duplicates'];
$subscribeall = (isset($_data['subscribeall'])) ? intval($_data['subscribeall']) : $is_now['subscribeall'];
$dry = (isset($_data['dry'])) ? intval($_data['dry']) : $is_now['dry'];
$delete1 = (isset($_data['delete1'])) ? intval($_data['delete1']) : $is_now['delete1'];
$delete2 = (isset($_data['delete2'])) ? intval($_data['delete2']) : $is_now['delete2'];
$automap = (isset($_data['automap'])) ? intval($_data['automap']) : $is_now['automap'];
$skipcrossduplicates = (isset($_data['skipcrossduplicates'])) ? intval($_data['skipcrossduplicates']) : $is_now['skipcrossduplicates'];
$port1 = (!empty($_data['port1'])) ? $_data['port1'] : $is_now['port1'];
$password1 = (!empty($_data['password1'])) ? $_data['password1'] : $is_now['password1'];
$host1 = (!empty($_data['host1'])) ? $_data['host1'] : $is_now['host1'];
$subfolder2 = (isset($_data['subfolder2'])) ? $_data['subfolder2'] : $is_now['subfolder2'];
$enc1 = (!empty($_data['enc1'])) ? $_data['enc1'] : $is_now['enc1'];
$mins_interval = (!empty($_data['mins_interval'])) ? $_data['mins_interval'] : $is_now['mins_interval'];
$exclude = (isset($_data['exclude'])) ? $_data['exclude'] : $is_now['exclude'];
$custom_params = (isset($_data['custom_params'])) ? $_data['custom_params'] : $is_now['custom_params'];
$maxage = (isset($_data['maxage']) && $_data['maxage'] != "") ? intval($_data['maxage']) : $is_now['maxage'];
$maxbytespersecond = (isset($_data['maxbytespersecond']) && $_data['maxbytespersecond'] != "") ? intval($_data['maxbytespersecond']) : $is_now['maxbytespersecond'];
$timeout1 = (isset($_data['timeout1']) && $_data['timeout1'] != "") ? intval($_data['timeout1']) : $is_now['timeout1'];
$timeout2 = (isset($_data['timeout2']) && $_data['timeout2'] != "") ? intval($_data['timeout2']) : $is_now['timeout2'];
}
else {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
// validate custom params
foreach (explode('-', $custom_params) as $param){
if(empty($param)) continue;
// extract option
if (str_contains($param, '=')) $param = explode('=', $param)[0];
else $param = rtrim($param, ' ');
// remove first char if first char is -
if ($param[0] == '-') $param = ltrim($param, $param[0]);
if (str_contains($param, ' ')) {
// bad char
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'bad character SPACE'
);
return false;
}
// check if param is whitelisted
if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){
// bad option
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'bad option '. $param
);
return false;
}
}
if (empty($subfolder2)) {
$subfolder2 = "";
}
if (!isset($maxage) || !filter_var($maxage, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$maxage = "0";
}
if (!isset($timeout1) || !filter_var($timeout1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$timeout1 = "600";
}
if (!isset($timeout2) || !filter_var($timeout2, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 32000)))) {
$timeout2 = "600";
}
if (!isset($maxbytespersecond) || !filter_var($maxbytespersecond, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 125000000)))) {
$maxbytespersecond = "0";
}
if (!filter_var($port1, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 65535)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (!filter_var($mins_interval, FILTER_VALIDATE_INT, array('options' => array('min_range' => 1, 'max_range' => 43800)))) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (!is_valid_domain_name($host1)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if ($enc1 != "TLS" && $enc1 != "SSL" && $enc1 != "PLAIN") {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
if (@preg_match("/" . $exclude . "/", null) === false) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("UPDATE `imapsync` SET `delete1` = :delete1,
`delete2` = :delete2,
`automap` = :automap,
`skipcrossduplicates` = :skipcrossduplicates,
`maxage` = :maxage,
`maxbytespersecond` = :maxbytespersecond,
`subfolder2` = :subfolder2,
`exclude` = :exclude,
`host1` = :host1,
`last_run` = :last_run,
`success` = :success,
`user1` = :user1,
`password1` = :password1,
`mins_interval` = :mins_interval,
`port1` = :port1,
`enc1` = :enc1,
`delete2duplicates` = :delete2duplicates,
`custom_params` = :custom_params,
`timeout1` = :timeout1,
`timeout2` = :timeout2,
`subscribeall` = :subscribeall,
`dry` = :dry,
`active` = :active
WHERE `id` = :id");
$stmt->execute(array(
':delete1' => $delete1,
':delete2' => $delete2,
':automap' => $automap,
':skipcrossduplicates' => $skipcrossduplicates,
':id' => $id,
':exclude' => $exclude,
':maxage' => $maxage,
':maxbytespersecond' => $maxbytespersecond,
':subfolder2' => $subfolder2,
':host1' => $host1,
':user1' => $user1,
':password1' => $password1,
':last_run' => $last_run,
':success' => $success,
':mins_interval' => $mins_interval,
':port1' => $port1,
':enc1' => $enc1,
':delete2duplicates' => $delete2duplicates,
':custom_params' => $custom_params,
':timeout1' => $timeout1,
':timeout2' => $timeout2,
':subscribeall' => $subscribeall,
':dry' => $dry,
':active' => $active,
));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('mailbox_modified', $username)
);
}
break;
case 'filter':
if (!isset($_SESSION['acl']['filters']) || $_SESSION['acl']['filters'] != "1" ) {
$_SESSION['return'][] = array(
@@ -3123,7 +3505,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $username;
}
return true;
break;
case 'mailbox_rename':
$domain = $_data['domain'];
@@ -3446,6 +3827,7 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
$attr["rl_frame"] = (!empty($_data['rl_frame'])) ? $_data['rl_frame'] : $is_now['rl_frame'];
$attr["rl_value"] = (!empty($_data['rl_value'])) ? $_data['rl_value'] : $is_now['rl_value'];
$attr["force_pw_update"] = isset($_data['force_pw_update']) ? intval($_data['force_pw_update']) : $is_now['force_pw_update'];
$attr["force_tfa"] = isset($_data['force_tfa']) ? intval($_data['force_tfa']) : $is_now['force_tfa'];
$attr["sogo_access"] = isset($_data['sogo_access']) ? intval($_data['sogo_access']) : $is_now['sogo_access'];
$attr["active"] = isset($_data['active']) ? intval($_data['active']) : $is_now['active'];
$attr["tls_enforce_in"] = isset($_data['tls_enforce_in']) ? intval($_data['tls_enforce_in']) : $is_now['tls_enforce_in'];
@@ -4193,6 +4575,68 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
}
return false;
break;
case 'syncjob_details':
$syncjobdetails = array();
if (!is_numeric($_data)) {
return false;
}
if (isset($_extra) && in_array('no_log', $_extra)) {
$field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("returned_text", "password1")');
$fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
while($field = array_shift($fields)) {
$shown_fields[] = $field['Field'];
}
$stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ",
`active`
FROM `imapsync` WHERE id = :id");
}
elseif (isset($_extra) && in_array('with_password', $_extra)) {
$stmt = $pdo->prepare("SELECT *,
`active`
FROM `imapsync` WHERE id = :id");
}
else {
$field_query = $pdo->query('SHOW FIELDS FROM `imapsync` WHERE FIELD NOT IN ("password1")');
$fields = $field_query->fetchAll(PDO::FETCH_ASSOC);
while($field = array_shift($fields)) {
$shown_fields[] = $field['Field'];
}
$stmt = $pdo->prepare("SELECT " . implode(',', (array)$shown_fields) . ",
`active`
FROM `imapsync` WHERE id = :id");
}
$stmt->execute(array(':id' => $_data));
$syncjobdetails = $stmt->fetch(PDO::FETCH_ASSOC);
if (!empty($syncjobdetails['returned_text'])) {
$syncjobdetails['log'] = $syncjobdetails['returned_text'];
}
else {
$syncjobdetails['log'] = '';
}
unset($syncjobdetails['returned_text']);
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $syncjobdetails['user2'])) {
return false;
}
return $syncjobdetails;
break;
case 'syncjobs':
$syncjobdata = array();
if (isset($_data) && filter_var($_data, FILTER_VALIDATE_EMAIL)) {
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $_data)) {
return false;
}
}
else {
$_data = $_SESSION['mailcow_cc_username'];
}
$stmt = $pdo->prepare("SELECT `id` FROM `imapsync` WHERE `user2` = :username");
$stmt->execute(array(':username' => $_data));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
while($row = array_shift($rows)) {
$syncjobdata[] = $row['id'];
}
return $syncjobdata;
break;
case 'spam_score':
$curl = curl_init();
curl_setopt($curl, CURLOPT_UNIX_SOCKET_PATH, '/var/lib/rspamd/rspamd.sock');
@@ -5014,6 +5458,46 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
break;
case 'delete':
switch ($_type) {
case 'syncjob':
if (!is_array($_data['id'])) {
$ids = array();
$ids[] = $_data['id'];
}
else {
$ids = $_data['id'];
}
if (!isset($_SESSION['acl']['syncjobs']) || $_SESSION['acl']['syncjobs'] != "1" ) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
return false;
}
foreach ($ids as $id) {
if (!is_numeric($id)) {
return false;
}
$stmt = $pdo->prepare("SELECT `user2` FROM `imapsync` WHERE id = :id");
$stmt->execute(array(':id' => $id));
$user2 = $stmt->fetch(PDO::FETCH_ASSOC)['user2'];
if (!hasMailboxObjectAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $user2)) {
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => 'access_denied'
);
continue;
}
$stmt = $pdo->prepare("DELETE FROM `imapsync` WHERE `id`= :id");
$stmt->execute(array(':id' => $id));
$_SESSION['return'][] = array(
'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr),
'msg' => array('deleted_syncjob', $id)
);
}
break;
case 'filter':
if (!is_array($_data['id'])) {
$ids = array();
@@ -5643,7 +6127,6 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) {
// Track affected mailboxes for SOGo update
$update_sogo_mailboxes[] = $username;
}
return true;
break;
case 'mailbox_templates':
if ($_SESSION['mailcow_cc_role'] != "admin") {
File diff suppressed because it is too large Load Diff
+6 -153
View File
@@ -4,7 +4,7 @@ function init_db_schema()
try {
global $pdo;
$db_version = "15072026_1000";
$db_version = "19022026_1220";
$stmt = $pdo->query("SHOW TABLES LIKE 'versions'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
@@ -754,133 +754,24 @@ function init_db_schema()
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync_source" => array(
"cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT",
"name" => "VARCHAR(100) NOT NULL",
"description" => "VARCHAR(255) NOT NULL DEFAULT ''",
"created_by" => "VARCHAR(255) NOT NULL DEFAULT ''",
"scope" => "ENUM('all','domain','user') NOT NULL DEFAULT 'user'",
"host1" => "VARCHAR(255) NOT NULL",
"port1" => "SMALLINT UNSIGNED NOT NULL",
"enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'",
"auth_type" => "ENUM('PLAIN','LOGIN','CRAM-MD5','XOAUTH2') DEFAULT 'PLAIN'",
"oauth_flow" => "ENUM('client_credentials','authorization_code') NOT NULL DEFAULT 'client_credentials'",
"oauth_token_endpoint" => "VARCHAR(500) DEFAULT NULL",
"oauth_authorize_endpoint" => "VARCHAR(500) DEFAULT NULL",
"oauth_userinfo_endpoint" => "VARCHAR(500) DEFAULT NULL",
"oauth_client_id" => "VARCHAR(500) DEFAULT NULL",
"oauth_client_secret" => "VARCHAR(500) DEFAULT NULL",
"oauth_scope" => "VARCHAR(500) DEFAULT NULL",
"oauth_extra_params" => "TEXT DEFAULT NULL",
"oauth_access_token" => "TEXT DEFAULT NULL",
"oauth_token_expires" => "BIGINT UNSIGNED DEFAULT NULL",
"oauth_last_refresh_error" => "TEXT DEFAULT 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("id")
),
"unique" => array(
"uniq_creator_name" => array("created_by", "name")
),
"key" => array(
"idx_created_by" => array("created_by"),
"idx_scope" => array("scope")
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync_source_domain" => array(
"cols" => array(
"source_id" => "INT NOT NULL",
"domain" => "VARCHAR(255) NOT NULL"
),
"keys" => array(
"primary" => array(
"" => array("source_id", "domain")
),
"key" => array(
"idx_domain" => array("domain")
),
"fkey" => array(
"fk_imapsync_source_domain" => array(
"col" => "source_id",
"ref" => "imapsync_source.id",
"delete" => "CASCADE",
"update" => "NO ACTION"
)
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync_source_user" => array(
"cols" => array(
"source_id" => "INT NOT NULL",
"username" => "VARCHAR(255) NOT NULL"
),
"keys" => array(
"primary" => array(
"" => array("source_id", "username")
),
"key" => array(
"idx_username" => array("username")
),
"fkey" => array(
"fk_imapsync_source_user" => array(
"col" => "source_id",
"ref" => "imapsync_source.id",
"delete" => "CASCADE",
"update" => "NO ACTION"
)
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync_source_oauth_token" => array(
"cols" => array(
"source_id" => "INT NOT NULL",
"username" => "VARCHAR(255) NOT NULL",
"access_token" => "TEXT DEFAULT NULL",
"refresh_token" => "TEXT DEFAULT NULL",
"token_expires" => "BIGINT UNSIGNED DEFAULT NULL",
"last_refresh_error" => "TEXT DEFAULT NULL",
"created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)",
"modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP"
),
"keys" => array(
"primary" => array(
"" => array("source_id", "username")
),
"fkey" => array(
"fk_imapsync_source_oauth_token" => array(
"col" => "source_id",
"ref" => "imapsync_source.id",
"delete" => "CASCADE",
"update" => "NO ACTION"
)
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
),
"imapsync" => array(
"cols" => array(
"id" => "INT NOT NULL AUTO_INCREMENT",
"user2" => "VARCHAR(255) NOT NULL",
"source_id" => "INT 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 DEFAULT ''",
"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'",
@@ -903,17 +794,6 @@ function init_db_schema()
"keys" => array(
"primary" => array(
"" => array("id")
),
"key" => array(
"idx_source_id" => array("source_id")
),
"fkey" => array(
"fk_imapsync_source" => array(
"col" => "source_id",
"ref" => "imapsync_source.id",
"delete" => "RESTRICT",
"update" => "NO ACTION"
)
)
),
"attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC"
@@ -1303,33 +1183,6 @@ function init_db_schema()
}
}
// Migrate imapsync to source-based schema
if ($table == 'imapsync') {
$stmt = $pdo->query("SHOW TABLES LIKE 'imapsync'");
if (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0) {
$stmt = $pdo->query("SHOW COLUMNS FROM `imapsync` LIKE 'host1'");
$has_legacy_cols = (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0);
$stmt = $pdo->query("SHOW COLUMNS FROM `imapsync` LIKE 'source_id'");
$has_source_id = (count($stmt->fetchAll(PDO::FETCH_ASSOC)) != 0);
if ($has_legacy_cols && !$has_source_id) {
$pdo->exec("INSERT IGNORE INTO `imapsync_source` (`name`, `created_by`, `scope`, `host1`, `port1`, `enc1`, `auth_type`, `active`)
SELECT CONCAT('legacy-', `host1`, '-', `port1`, '-', `enc1`) AS name,
'' AS created_by, 'all' AS scope, `host1`, `port1`, `enc1`, `authmech1`, 1
FROM `imapsync`
GROUP BY `host1`, `port1`, `enc1`, `authmech1`");
$pdo->exec("ALTER TABLE `imapsync` ADD COLUMN `source_id` INT DEFAULT NULL");
$pdo->exec("UPDATE `imapsync` i
JOIN `imapsync_source` s
ON s.`host1` = i.`host1` AND s.`port1` = i.`port1`
AND s.`enc1` = i.`enc1` AND s.`auth_type` = i.`authmech1`
AND s.`created_by` = '' AND s.`scope` = 'all'
SET i.`source_id` = s.`id`");
$pdo->exec("ALTER TABLE `imapsync` MODIFY COLUMN `source_id` INT NOT NULL");
}
}
}
$stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'");
$num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC));
if ($num_results != 0) {
-1
View File
@@ -285,7 +285,6 @@ require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.domain_admin.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fail2ban.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.fwdhost.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.syncjob.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailq.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.oauth2.inc.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.policy.inc.php';
+3
View File
@@ -286,6 +286,7 @@ $IMAPSYNC_OPTIONS = array(
'abort',
'authmd51',
'authmd52',
'authmech1',
'authmech2',
'authuser1',
'authuser2',
@@ -357,7 +358,9 @@ $IMAPSYNC_OPTIONS = array(
'notls2',
'nouidexpunge2',
'nousecache',
'oauthaccesstoken1',
'oauthaccesstoken2',
'oauthdirect1',
'oauthdirect2',
'office1',
'office2',
-131
View File
@@ -414,134 +414,3 @@ function copyToClipboard(id) {
navigator.clipboard.writeText(copyText.value);
mailcow_alert_box(lang.copy_to_clipboard, "success");
}
// ===== IMAP sync sources: shared helpers + delegated listeners =====
// Used by the mailbox/user/edit pages (each still owns its own draw_imapsync_source_table).
// Kept here because 013-mailcow.js is loaded globally, so the logic lives in one place.
// Human-readable visibility label for a source row (used by the per-page source tables)
function imapsyncScopeDisplay(item) {
if (item.scope === 'all') return '<i class="bi bi-globe"></i> ' + lang.syncjobs.source_scope_all;
if (item.scope === 'domain') return escapeHtml(lang.syncjobs.source_scope_domain + ': ' + (item.domains || []).join(', '));
var us = item.users || [];
if (us.length === 0) return escapeHtml(item.created_by || '');
return escapeHtml(lang.syncjobs.source_scope_user + ': ' + us.join(', '));
}
// (Re)build every imapsync-source dropdown from the ACL-filtered API (fresh on modal open)
function populateImapsyncSourceSelects() {
$.get("/api/v1/get/syncjob_source/all", function(sources) {
$('select.imapsync-source-select').each(function() {
var $sel = $(this);
var keepId = $sel.data('current-source-id');
$sel.empty();
$sel.append('<option value="">' + (lang.syncjobs ? lang.syncjobs.source_select : '') + '</option>');
$.each(sources, function(_, src) {
var label = src.name + ' — ' + src.host1 + ':' + src.port1 + ' (' + src.auth_type + ')';
if (src.scope === 'all') label += ' [' + (lang.syncjobs ? lang.syncjobs.source_scope_all : 'all') + ']';
var $opt = $('<option/>').val(src.id).text(label)
.data('auth-type', src.auth_type).data('oauth-flow', src.oauth_flow);
if (keepId && parseInt(keepId) === parseInt(src.id)) $opt.attr('selected', 'selected');
$sel.append($opt);
});
if (typeof $sel.selectpicker === 'function') {
$sel.selectpicker('destroy');
$sel.selectpicker();
}
$sel.trigger('change');
});
});
}
// Populate a scope domain/user multiselect from the ACL-filtered endpoint (preserves existing options)
function fillImapsyncScopeSelect($sel, url, field) {
$.get('/api/v1/get/' + url, function(rows) {
var have = {};
$sel.find('option').each(function() { have[$(this).val()] = true; });
$.each(rows, function(_, r) {
var v = r[field];
if (!v || have[v]) return;
$sel.append($('<option/>').val(v).text(v));
});
// Rebuild the bootstrap-select wrapper (destroy + re-init)
if (typeof $sel.selectpicker === 'function') {
$sel.selectpicker('destroy');
$sel.selectpicker();
}
});
}
// Source editor: OAuth block visibility + authorization_code-only fields + redirect URI
function imapsyncSyncSourceOauthToggle($form) {
var isOauth = $form.find('select.imapsync-source-auth-type').val() === 'XOAUTH2';
var isAuthCode = $form.find('select.imapsync-source-oauth-flow').val() === 'authorization_code';
$form.find('.imapsync-source-oauth-block').toggle(isOauth);
$form.find('.imapsync-source-authcode-block').toggle(isOauth && isAuthCode);
$form.find('.imapsync-source-redirect-uri').val(window.location.origin + '/syncjob-oauth');
}
$(document).ready(function() {
// Syncjob form: toggle password row / OAuth "connect" button by the selected source's flow
$(document).on('change', 'select.imapsync-source-select', function() {
var $sel = $(this);
var $opt = $sel.find('option:selected');
var $form = $sel.closest('form');
var isOauth = ($opt.data('auth-type') === 'XOAUTH2');
var isAuthCode = isOauth && ($opt.data('oauth-flow') === 'authorization_code');
$form.find('.password1-row').toggle(!isOauth);
$form.find('input[name="password1"]').prop('required', !isOauth);
$form.find('.oauth-source-hint').toggle(isOauth);
$form.find('.imapsync-oauth-connect-row').toggle(isAuthCode);
});
// Source editor: OAuth block + authorization_code fields
$(document).on('change', 'select.imapsync-source-auth-type, select.imapsync-source-oauth-flow', function() {
imapsyncSyncSourceOauthToggle($(this).closest('form'));
});
// Source editor: scope -> show + populate the domain/user pickers
$(document).on('change', 'select.imapsync-source-scope', function() {
var $form = $(this).closest('form');
var scope = $(this).val();
$form.find('.imapsync-source-domains-row').toggle(scope === 'domain');
$form.find('.imapsync-source-users-row').toggle(scope === 'user');
if (scope === 'domain') fillImapsyncScopeSelect($form.find('.imapsync-source-domains'), 'domain/all', 'domain_name');
if (scope === 'user') fillImapsyncScopeSelect($form.find('.imapsync-source-users'), 'mailbox/all', 'username');
});
// "Connect with the provider": open the per-user OAuth popup for the selected source
$(document).on('click', '.imapsync-oauth-connect-btn', function() {
var $form = $(this).closest('form');
var sourceId = $form.find('select.imapsync-source-select').val();
if (!sourceId) return;
window.open('/syncjob-oauth?action=start&source_id=' + encodeURIComponent(sourceId),
'syncjob_oauth', 'width=600,height=700');
});
// Result from the OAuth popup: fill + lock user1, mark connected (or show the error)
window.addEventListener('message', function(ev) {
if (ev.origin !== window.location.origin || !ev.data || ev.data.type !== 'syncjob_oauth') return;
$('.imapsync-oauth-connect-row:visible').each(function() {
var $form = $(this).closest('form');
var $status = $form.find('.imapsync-oauth-connect-status');
if (ev.data.ok) {
$form.find('input[name="user1"]').val(ev.data.user1).prop('readonly', true);
$status.removeClass('text-danger').addClass('text-success')
.text((lang.syncjobs ? lang.syncjobs.syncjob_oauth_connected : 'Connected') + ': ' + ev.data.user1);
} else {
$status.removeClass('text-success').addClass('text-danger').text(ev.data.error || 'error');
}
});
});
// Add-source modal opens: init scope pickers + OAuth block (auth_type may be cached-form preselected)
$(document).on('shown.bs.modal', '#addImapsyncSourceModal', function() {
$(this).find('select.imapsync-source-scope').trigger('change');
imapsyncSyncSourceOauthToggle($(this).find('form'));
});
// Add-syncjob modal opens: (re)load the source dropdown from the API
$(document).on('shown.bs.modal', '#addSyncJobModalAdmin, #addSyncJobModal', function() {
populateImapsyncSourceSelects();
});
});
-11
View File
@@ -240,15 +240,4 @@ jQuery(function($){
// Draw Table if tab is active
onVisible("[id^=wl_policy_domain_table]", () => draw_wl_policy_domain_table());
onVisible("[id^=bl_policy_domain_table]", () => draw_bl_policy_domain_table());
// initialise imapsync-source form on load.
if ($('select.imapsync-source-oauth-flow').length) {
imapsyncSyncSourceOauthToggle($('select.imapsync-source-oauth-flow').closest('form'));
}
if ($('select.imapsync-source-scope').length) {
$('select.imapsync-source-scope').trigger('change');
}
if ($('select.imapsync-source-select').length) {
populateImapsyncSourceSelects();
}
});
+16 -65
View File
@@ -424,6 +424,11 @@ $(document).ready(function() {
} else {
$('#force_pw_update').prop('checked', false);
}
if (template.force_tfa == 1){
$('#force_tfa').prop('checked', true);
} else {
$('#force_tfa').prop('checked', false);
}
if (template.sogo_access == 1){
$('#sogo_access').prop('checked', true);
} else {
@@ -1242,6 +1247,7 @@ jQuery(function($){
item.attributes.eas_access = '<i class="text-' + (item.attributes.eas_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.eas_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.eas_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.dav_access = '<i class="text-' + (item.attributes.dav_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.dav_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.dav_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.sogo_access = '<i class="text-' + (item.attributes.sogo_access == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.sogo_access == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.sogo_access == 1 ? '1' : '0') + '</span></i>';
item.attributes.force_tfa = '<i class="text-' + (item.attributes.force_tfa == 1 ? 'success' : 'danger') + ' bi bi-' + (item.attributes.force_tfa == 1 ? 'check-lg' : 'x-lg') + '"><span class="sorting-value">' + (item.attributes.force_tfa == 1 ? '1' : '0') + '</span></i>';
if (item.attributes.quarantine_notification === 'never') {
item.attributes.quarantine_notification = lang.never;
} else if (item.attributes.quarantine_notification === 'hourly') {
@@ -1385,6 +1391,11 @@ jQuery(function($){
return 1==data?'<i class="bi bi-check-lg"></i>':'<i class="bi bi-x-lg"></i>';
}
},
{
title: lang.force_tfa,
data: 'attributes.force_tfa',
defaultContent: ''
},
{
title: lang_edit.ratelimit,
data: 'attributes.ratelimit',
@@ -2173,7 +2184,7 @@ jQuery(function($){
} else {
item.exclude = '<code>' + escapeHtml(item.exclude) + '</code>';
}
item.server_w_port = escapeHtml(item.user1) + '@' + escapeHtml(item.source_name || '?') + ' (' + escapeHtml(item.source_host || '?') + ':' + escapeHtml(item.source_port || '?') + ')';
item.server_w_port = escapeHtml(item.user1) + '@' + escapeHtml(item.host1) + ':' + escapeHtml(item.port1);
item.action = '<div class="btn-group">' +
'<a href="/edit/syncjob/' + item.id + '" class="btn btn-sm btn-xs-lg btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'<a href="#" data-action="delete_selected" data-id="single-syncjob" data-api-url="delete/syncjob" data-item="' + item.id + '" class="btn btn-sm btn-xs-lg btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
@@ -2193,10 +2204,10 @@ jQuery(function($){
} else {
item.success = '<i class="text-' + (item.success == 1 ? 'success' : 'danger') + ' bi bi-' + (item.success == 1 ? 'check-lg' : 'x-lg') + '"></i>';
}
if (lang.syncjobs['syncjob_'+item.exit_status]) {
item.exit_status = lang.syncjobs['syncjob_'+item.exit_status];
if (lang['syncjob_'+item.exit_status]) {
item.exit_status = lang['syncjob_'+item.exit_status];
} else if (item.success != '-') {
item.exit_status = lang.syncjobs.syncjob_check_log;
item.exit_status = lang.syncjob_check_log;
}
item.exit_status = item.success + ' ' + item.exit_status;
});
@@ -2245,7 +2256,7 @@ jQuery(function($){
defaultContent: ''
},
{
title: lang.syncjobs.syncjob_last_run_result,
title: lang.syncjob_last_run_result,
data: 'exit_status',
defaultContent: ''
},
@@ -2465,65 +2476,6 @@ jQuery(function($){
});
}
function draw_imapsync_source_table() {
if ($.fn.DataTable.isDataTable('#imapsync_source_table')) {
$('#imapsync_source_table').DataTable().columns.adjust().responsive.recalc();
return;
}
$('#imapsync_source_table').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
ajax: {
type: "GET",
url: "/api/v1/get/syncjob_source/all",
dataSrc: function(json) {
$.each(json, function(i, item) {
item.endpoint = escapeHtml(item.host1 + ':' + item.port1 + ' (' + item.enc1 + ')');
item.auth_display = escapeHtml(item.auth_type);
if (item.auth_type === 'XOAUTH2') {
if (item.oauth_token_expires && item.oauth_token_expires * 1000 > Date.now()) {
item.auth_display += ' <span class="badge bg-success">' + lang.syncjobs.source_token_status + ': ' + new Date(item.oauth_token_expires * 1000).toLocaleString() + '</span>';
} else if (item.oauth_last_refresh_error) {
item.auth_display += ' <span class="badge bg-danger" title="' + escapeHtml(item.oauth_last_refresh_error) + '">' + lang.syncjobs.source_token_error + '</span>';
} else {
item.auth_display += ' <span class="badge bg-warning">' + lang.waiting + '</span>';
}
}
item.visibility_display = imapsyncScopeDisplay(item);
if (item.can_edit) {
item.action = '<div class="btn-group">' +
'<a href="/edit/syncjob_source/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'<a href="#" data-action="delete_selected" data-id="single-syncjob_source" data-api-url="delete/syncjob_source" data-item="' + item.id + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
'</div>';
} else {
item.action = '<span class="text-muted">-</span>';
}
});
return json;
}
},
columns: [
{ title: 'ID', data: 'id', defaultContent: '' },
{ title: lang.syncjobs.source_name, data: 'name', defaultContent: '', render: (d) => escapeHtml(d || '') },
{ title: lang.syncjobs.source_scope, data: 'visibility_display', defaultContent: '' },
{ title: 'Endpoint', data: 'endpoint', defaultContent: '' },
{ title: lang.syncjobs.source_auth_type, data: 'auth_display', defaultContent: '' },
{ title: lang.active, data: 'active', defaultContent: '',
render: function(d) { return d == 1 ? '<i class="bi bi-check-lg"></i>' : '<i class="bi bi-x-lg"></i>'; }},
{ title: lang.action, data: 'action', defaultContent: '', className: 'dt-text-right' }
]
});
}
// Shared imapsync helpers + delegated listeners live in js/build/013-mailcow.js
// (draw_imapsync_source_table stays per-page and calls the global imapsyncScopeDisplay).
// Load only if the tab is visible
onVisible("[id^=domain_table]", () => draw_domain_table());
onVisible("[id^=templates_domain_table]", () => draw_templates_domain_table());
@@ -2533,7 +2485,6 @@ jQuery(function($){
onVisible("[id^=alias_table]", () => draw_alias_table());
onVisible("[id^=aliasdomain_table]", () => draw_aliasdomain_table());
onVisible("[id^=sync_job_table]", () => draw_sync_job_table());
onVisible("[id^=imapsync_source_table]", () => draw_imapsync_source_table());
onVisible("[id^=filter_table]", () => draw_filter_table());
onVisible("[id^=bcc_table]", () => draw_bcc_table());
onVisible("[id^=recipient_map_table]", () => draw_recipient_map_table());
+5 -65
View File
@@ -284,7 +284,7 @@ jQuery(function($){
} else {
item.exclude = '<code>' + escapeHtml(item.exclude) + '</code>';
}
item.server_w_port = escapeHtml(item.user1 + '@' + (item.source_name || '?') + ' (' + (item.source_host || '?') + ':' + (item.source_port || '?') + ')');
item.server_w_port = escapeHtml(item.user1 + '@' + item.host1 + ':' + item.port1);
if (acl_data.syncjobs === 1) {
item.action = '<div class="btn-group">' +
'<a href="/edit/syncjob/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
@@ -310,10 +310,10 @@ jQuery(function($){
} else {
item.success = '<i class="text-' + (item.success == 1 ? 'success' : 'danger') + ' bi bi-' + (item.success == 1 ? 'check-lg' : 'x-lg') + '"></i>';
}
if (lang.syncjobs['syncjob_'+item.exit_status]) {
item.exit_status = lang.syncjobs['syncjob_'+item.exit_status];
if (lang['syncjob_'+item.exit_status]) {
item.exit_status = lang['syncjob_'+item.exit_status];
} else if (item.success != '-') {
item.exit_status = lang.syncjobs.syncjob_check_log;
item.exit_status = lang.syncjob_check_log;
}
item.exit_status = item.success + ' ' + item.exit_status;
});
@@ -362,7 +362,7 @@ jQuery(function($){
defaultContent: ''
},
{
title: lang.syncjobs.syncjob_last_run_result,
title: lang.syncjob_last_run_result,
data: 'exit_status',
defaultContent: ''
},
@@ -694,71 +694,11 @@ jQuery(function($){
});
}
function draw_imapsync_source_table() {
if ($.fn.DataTable.isDataTable('#imapsync_source_table')) {
$('#imapsync_source_table').DataTable().columns.adjust().responsive.recalc();
return;
}
$('#imapsync_source_table').DataTable({
responsive: true,
processing: true,
serverSide: false,
stateSave: true,
pageLength: pagination_size,
dom: "<'row'<'col-sm-12 col-md-6'f><'col-sm-12 col-md-6'l>>" + "tr" +
"<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
language: lang_datatables,
ajax: {
type: "GET",
url: "/api/v1/get/syncjob_source/all",
dataSrc: function(json) {
$.each(json, function(i, item) {
item.endpoint = escapeHtml(item.host1 + ':' + item.port1 + ' (' + item.enc1 + ')');
item.auth_display = escapeHtml(item.auth_type);
if (item.auth_type === 'XOAUTH2') {
if (item.oauth_token_expires && item.oauth_token_expires * 1000 > Date.now()) {
item.auth_display += ' <span class="badge bg-success">' + lang.syncjobs.source_token_status + ': ' + new Date(item.oauth_token_expires * 1000).toLocaleString() + '</span>';
} else if (item.oauth_last_refresh_error) {
item.auth_display += ' <span class="badge bg-danger" title="' + escapeHtml(item.oauth_last_refresh_error) + '">' + lang.syncjobs.source_token_error + '</span>';
} else {
item.auth_display += ' <span class="badge bg-warning">' + lang.waiting + '</span>';
}
}
item.visibility_display = imapsyncScopeDisplay(item);
if (acl_data.syncjobs === 1 && item.can_edit) {
item.action = '<div class="btn-group">' +
'<a href="/edit/syncjob_source/' + item.id + '" class="btn btn-xs btn-xs-half btn-secondary"><i class="bi bi-pencil-fill"></i> ' + lang.edit + '</a>' +
'<a href="#" data-action="delete_selected" data-id="single-syncjob_source" data-api-url="delete/syncjob_source" data-item="' + item.id + '" class="btn btn-xs btn-xs-half btn-danger"><i class="bi bi-trash"></i> ' + lang.remove + '</a>' +
'</div>';
} else {
item.action = '<span class="text-muted">-</span>';
}
});
return json;
}
},
columns: [
{ title: 'ID', data: 'id', defaultContent: '' },
{ title: lang.syncjobs.source_name, data: 'name', defaultContent: '', render: (d) => escapeHtml(d || '') },
{ title: lang.syncjobs.source_scope, data: 'visibility_display', defaultContent: '' },
{ title: 'Endpoint', data: 'endpoint', defaultContent: '' },
{ title: lang.syncjobs.source_auth_type, data: 'auth_display', defaultContent: '' },
{ title: lang.active, data: 'active', defaultContent: '',
render: function(d) { return d == 1 ? '<i class="bi bi-check-lg"></i>' : '<i class="bi bi-x-lg"></i>'; }},
{ title: lang.action, data: 'action', defaultContent: '', className: 'dt-text-right' }
]
});
}
// Shared imapsync helpers + delegated listeners live in js/build/013-mailcow.js
// (draw_imapsync_source_table stays per-page and calls the global imapsyncScopeDisplay).
// Load only if the tab is visible
onVisible("[id^=tla_table]", () => draw_tla_table());
onVisible("[id^=bl_policy_mailbox_table]", () => draw_bl_policy_mailbox_table());
onVisible("[id^=wl_policy_mailbox_table]", () => draw_wl_policy_mailbox_table());
onVisible("[id^=sync_job_table]", () => draw_sync_job_table());
onVisible("[id^=imapsync_source_table]", () => draw_imapsync_source_table());
onVisible("[id^=app_passwd_table]", () => draw_app_passwd_table());
onVisible("[id^=recent-logins]", () => last_logins('get'));
});
+11 -54
View File
@@ -317,10 +317,7 @@ if (isset($_GET['query'])) {
process_add_return(admin('add', $attr));
break;
case "syncjob":
process_add_return(syncjob('add', 'job', $attr));
break;
case "syncjob_source":
process_add_return(syncjob('add', 'source', $attr));
process_add_return(mailbox('add', 'syncjob', $attr));
break;
case "bcc":
process_add_return(bcc('add', $attr));
@@ -1049,33 +1046,6 @@ if (isset($_GET['query'])) {
}
process_get_return($data);
break;
case "syncjob_source":
switch ($object) {
case "all":
$ids = syncjob('get', 'sources');
$data = array();
if (is_array($ids)) {
foreach ($ids as $sid) {
$row = syncjob('get', 'source', array('id' => $sid));
if ($row) $data[] = $row;
}
}
process_get_return($data);
break;
default:
if (is_numeric($object)) {
$row = syncjob('get', 'source', array('id' => $object));
if ($row) {
process_get_return($row);
} else {
echo '{}';
}
} else {
echo '{}';
}
break;
}
break;
case "syncjobs":
switch ($object) {
case "all":
@@ -1085,14 +1055,14 @@ if (isset($_GET['query'])) {
$mailboxes = mailbox('get', 'mailboxes', $domain);
if (!empty($mailboxes)) {
foreach ($mailboxes as $mailbox) {
$syncjobs = syncjob('get', 'jobs', $mailbox);
$syncjobs = mailbox('get', 'syncjobs', $mailbox);
if (!empty($syncjobs)) {
foreach ($syncjobs as $sj) {
foreach ($syncjobs as $syncjob) {
if (isset($extra)) {
$details = syncjob('get', 'job', $sj, explode(',', $extra));
$details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra));
}
else {
$details = syncjob('get', 'job', $sj);
$details = mailbox('get', 'syncjob_details', $syncjob);
}
if ($details) {
$data[] = $details;
@@ -1113,14 +1083,14 @@ if (isset($_GET['query'])) {
break;
default:
$syncjobs = syncjob('get', 'jobs', $object);
$syncjobs = mailbox('get', 'syncjobs', $object);
if (!empty($syncjobs)) {
foreach ($syncjobs as $sj) {
foreach ($syncjobs as $syncjob) {
if (isset($extra)) {
$details = syncjob('get', 'job', $sj, explode(',', $extra));
$details = mailbox('get', 'syncjob_details', $syncjob, explode(',', $extra));
}
else {
$details = syncjob('get', 'job', $sj);
$details = mailbox('get', 'syncjob_details', $syncjob);
}
if ($details) {
$data[] = $details;
@@ -1772,10 +1742,7 @@ if (isset($_GET['query'])) {
process_delete_return(rsettings('delete', array('id' => $items)));
break;
case "syncjob":
process_delete_return(syncjob('delete', 'job', array('id' => $items)));
break;
case "syncjob_source":
process_delete_return(syncjob('delete', 'source', array('id' => $items)));
process_delete_return(mailbox('delete', 'syncjob', array('id' => $items)));
break;
case "filter":
process_delete_return(mailbox('delete', 'filter', array('id' => $items)));
@@ -2001,17 +1968,7 @@ if (isset($_GET['query'])) {
}
break;
case "syncjob":
process_edit_return(syncjob('edit', 'job', array_merge(array('id' => $items), $attr)));
break;
case "syncjob_source":
switch ($object) {
case "refresh_token":
process_edit_return(syncjob('edit', 'refresh_token', array('id' => $items)));
break;
default:
process_edit_return(syncjob('edit', 'source', array_merge(array('id' => $items), $attr)));
break;
}
process_edit_return(mailbox('edit', 'syncjob', array_merge(array('id' => $items), $attr)));
break;
case "filter":
process_edit_return(mailbox('edit', 'filter', array_merge(array('id' => $items), $attr)));
+51 -82
View File
@@ -44,16 +44,24 @@
"app_name": "App-Name",
"app_password": "App-Passwort hinzufügen",
"app_passwd_protocols": "Zugelassene Protokolle für App-Passwort",
"automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"backup_mx_options": "Relay-Optionen",
"bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.",
"comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.",
"custom_params": "Eigene Parameter",
"custom_params_hint": "Richtig: --param=xy, falsch: --param xy",
"delete1": "Lösche Nachricht nach Übertragung vom Quell-Server.",
"delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf dem Quell-Server vorhanden sind.",
"delete2duplicates": "Lösche Duplikate im Ziel",
"description": "Beschreibung",
"destination": "Ziel",
"disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
"domain": "Domain",
"domain_matches_hostname": "Domain %s darf nicht dem Hostnamen entsprechen",
"domain_quota_m": "Domain-Speicherplatz gesamt (MiB)",
"dry": "Synchronisation simulieren",
"enc_method": "Verschlüsselung",
"exclude": "Elemente ausschließen (Regex)",
"full_name": "Vor- und Nachname",
"gal": "Globales Adressbuch",
"gal_info": "Das globale Adressbuch enthält alle Objekte einer Domain und kann durch keinen Benutzer geändert werden. Die Verfügbarkeitsinformation in SOGo ist nur bei eingeschaltetem globalen Adressbuch ersichtlich! <b>Zum Anwenden einer Änderung muss SOGo neugestartet werden.</b>",
@@ -72,6 +80,7 @@
"mailbox_username": "Benutzername (linker Teil der E-Mail-Adresse)",
"max_aliases": "Max. mögliche Aliasse",
"max_mailboxes": "Max. mögliche Mailboxen",
"mins_interval": "Abrufintervall (Minuten)",
"multiple_bookings": "Mehrfaches Buchen möglich",
"nexthop": "Next Hop",
"password": "Passwort",
@@ -91,9 +100,15 @@
"select_domain": "Bitte zuerst eine Domain auswählen",
"sieve_desc": "Kurze Beschreibung",
"sieve_type": "Filtertyp",
"skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")",
"subscribeall": "Alle synchronisierten Ordner abonnieren",
"syncjob": "Sync-Job hinzufügen",
"syncjob_hint": "Passwörter werden unverschlüsselt abgelegt!",
"target_address": "Ziel-Adresse(n)",
"target_address_info": "<small>Vollständige E-Mail-Adresse(n). Getrennt durch Komma.</small>",
"target_domain": "Ziel-Domain",
"timeout1": "Timeout für Verbindung zum Remote-Host",
"timeout2": "Timeout für Verbindung zum lokalen Host",
"username": "Benutzername",
"validate": "Validieren",
"validation_success": "Erfolgreich validiert",
@@ -544,22 +559,7 @@
"template_exists": "Vorlage %s existiert bereits",
"template_id_invalid": "Vorlagen-ID %s ungültig",
"template_name_invalid": "Name der Vorlage ungültig",
"required_data_missing": "Die benötigten Daten: %s fehlen",
"imapsync_source_in_use": "Sync-Quelle %s wird noch von einem Sync-Job verwendet und kann nicht gelöscht werden",
"imapsync_source_token_refresh_failed": "OAuth-Token-Erneuerung für %s fehlgeschlagen",
"imapsync_source_scope_empty": "Für die gewählte Sichtbarkeit mindestens eine Domain wählen",
"imapsync_source_unavailable": "Die gewählte Sync-Quelle ist nicht verfügbar",
"imapsync_source_oauth_connect_required": "Vor dem Speichern dieses Sync-Jobs beim Provider anmelden",
"imapsync_source_name_invalid": "Ungültiger Name der Sync-Quelle",
"imapsync_source_host_invalid": "Ungültiger Host",
"imapsync_source_port_invalid": "Ungültiger Port",
"imapsync_source_enc_invalid": "Ungültige Verschlüsselungsmethode",
"imapsync_source_auth_invalid": "Ungültige Authentifizierungsmethode",
"imapsync_source_oauth_required": "OAuth2 erfordert Token-Endpoint, Client-ID, Client-Secret und Scope",
"imapsync_source_oauth_endpoint_invalid": "Ungültige OAuth2-Endpoint-URL",
"imapsync_source_oauth_extra_invalid": "OAuth2-Extra-Parameter müssen gültiges JSON sein",
"imapsync_source_invalid": "Ungültige Daten der Sync-Quelle",
"imapsync_source_oauth_invalid": "Ungültige OAuth2-Konfiguration"
"required_data_missing": "Die benötigten Daten: %s fehlen"
},
"datatables": {
"collapse_all": "Alle Einklappen",
@@ -648,6 +648,7 @@
"app_name": "App-Name",
"app_passwd": "App-Passwörter",
"app_passwd_protocols": "Zugelassene Protokolle für App-Passwort",
"automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"backup_mx_options": "Relay-Optionen",
"bcc_dest_format": "BCC-Ziel muss eine gültige E-Mail-Adresse sein.",
"client_id": "Client-ID",
@@ -655,6 +656,9 @@
"comment_info": "Ein privater Kommentar ist für den Benutzer nicht einsehbar. Ein öffentlicher Kommentar wird als Tooltip im Interface des Benutzers angezeigt.",
"created_on": "Erstellt am",
"custom_attributes": "benutzerdefinierte Attribute",
"delete1": "Lösche Nachricht nach Übertragung vom Quell-Server",
"delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf Quell-Server vorhanden sind",
"delete2duplicates": "Lösche Duplikate im Ziel",
"delete_ays": "Soll der Löschvorgang wirklich ausgeführt werden?",
"description": "Beschreibung",
"disable_login": "Login verbieten (Mails werden weiterhin angenommen)",
@@ -678,6 +682,7 @@
"dont_check_sender_acl": "Absender für Domain %s u. Alias-Domain nicht prüfen",
"edit_alias_domain": "Alias-Domain bearbeiten",
"encryption": "Verschlüsselung",
"exclude": "Elemente ausschließen (Regex)",
"extended_sender_acl": "Externe Absenderadressen",
"extended_sender_acl_info": "Der DKIM-Domainkey der externen Absenderdomain sollte in diesen Server importiert werden, falls vorhanden.<br>\r\n Wird SPF verwendet, muss diesem Server der Versand gestattet werden.<br>\r\n Wird eine Domain oder Alias-Domain zu diesem Server hinzugefügt, die sich mit der externen Absenderadresse überschneidet, wird der externe Absender hier entfernt.<br>\r\n Ein Eintrag @domain.tld erlaubt den Versand als *@domain.tld",
"force_pw_update": "Erzwinge Passwortänderung bei nächstem Login",
@@ -708,7 +713,10 @@
"max_aliases": "Max. Aliasse",
"max_mailboxes": "Max. Mailboxanzahl",
"max_quota": "Max. Größe per Mailbox (MiB)",
"maxage": "Maximales Alter in Tagen einer Nachricht, die kopiert werden soll<br><small>(0 = alle Nachrichten kopieren)</small>",
"maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)",
"mbox_rl_info": "Dieses Limit wird auf den SASL Loginnamen angewendet und betrifft daher alle Absenderadressen, die der eingeloggte Benutzer verwendet. Bei Mailbox Ratelimit überwiegt ein Domain-weites Ratelimit.",
"mins_interval": "Intervall (min)",
"mta_sts": "MTA-STS",
"mta_sts_info": "<a href='https://de.wikipedia.org/wiki/STARTTLS#MTA-STS' target='_blank'>MTA-STS</a> ist ein Standard, der den E-Mail-Versand zwischen Mailservern zwingt, TLS mit gültigen Zertifikaten zu verwenden. <br>Er wird verwendet, wenn <a target='_blank' href='https://de.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> aufgrund fehlender oder nicht unterstützter DNSSEC nicht möglich ist.<br><b>Hinweis</b>: Wenn die empfangende Domain DANE mit DNSSEC unterstützt, wird DANE <b>immer</b> bevorzugt MTA-STS fungiert nur als Fallback.",
"mta_sts_version": "Version",
@@ -759,6 +767,7 @@
"sender_acl_info": "Wird einem Mailbox-Benutzer A der Versand als Mailbox-Benutzer B gestattet, so erscheint der Absender <b>nicht</b> automatisch in SOGo zur Auswahl.<br>\r\n In SOGo muss zusätzlich eine Delegation eingerichtet werden. Dieses Verhalten trifft nicht auf Alias-Adressen zu.",
"sieve_desc": "Kurze Beschreibung",
"sieve_type": "Filtertyp",
"skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")",
"sogo_access": "Direktes weiterleiten an SOGo",
"sogo_access_info": "Nach dem Einloggen wird der Benutzer automatisch an SOGo weitergeleitet.",
"sogo_visible": "Alias in SOGo sichtbar",
@@ -767,8 +776,12 @@
"spam_filter": "Spamfilter",
"spam_policy": "Hinzufügen und Entfernen von Einträgen in Allow- und Denylisten",
"spam_score": "Einen benutzerdefiniterten Spam-Score festlegen",
"subfolder2": "Ziel-Ordner<br><small>(leer = kein Unterordner)</small>",
"syncjob": "Sync-Job bearbeiten",
"target_address": "Ziel-Adresse(n)",
"target_domain": "Ziel-Domain",
"timeout1": "Timeout für Verbindung zum Remote-Host",
"timeout2": "Timeout für Verbindung zum lokalen Host",
"title": "Objekt bearbeiten",
"unchanged_if_empty": "Unverändert, wenn leer",
"username": "Benutzername",
@@ -982,6 +995,7 @@
"spam_aliases": "Spam-Alias",
"stats": "Statistik",
"status": "Status",
"sync_jobs": "Synchronisationen",
"table_size": "Tabellengröße",
"table_size_show_n": "Zeige %s Einträge",
"target_address": "Ziel-Adresse",
@@ -1009,8 +1023,17 @@
"open_logs": "Öffne Logs",
"recipient": "Empfänger",
"sender": "Sender",
"syncjob_EX_OK": "Erfolg",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort",
"syncjob_check_log": "Logs überprüfen",
"all_domains": "Alle Domains",
"catch_all": "Catch-All",
"syncjob_last_run_result": "Letztes Ausführungsergebnis",
"syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem",
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
"syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit",
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen",
"iam": "Identitätsanbieter"
},
"oauth2": {
@@ -1184,11 +1207,7 @@
"verified_fido2_login": "FIDO2-Anmeldung verifiziert",
"verified_totp_login": "TOTP-Anmeldung verifiziert",
"verified_webauthn_login": "WebAuthn-Anmeldung verifiziert",
"verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert",
"imapsync_source_added": "Sync-Quelle %s wurde hinzugefügt",
"imapsync_source_modified": "Sync-Quelle %s wurde gespeichert",
"imapsync_source_deleted": "Sync-Quelle %s wurde gelöscht",
"imapsync_source_token_refreshed": "OAuth-Token für %s wurde erneuert"
"verified_yotp_login": "Yubico-OTP-Anmeldung verifiziert"
},
"tfa": {
"authenticators": "Authentikatoren",
@@ -1223,67 +1242,6 @@
"setup_required": "Ihr Konto erfordert Zwei-Faktor-Authentifizierung. Bitte richten Sie eine 2FA-Methode ein, um fortzufahren.",
"cancel_setup": "Abbrechen und abmelden"
},
"syncjobs": {
"sources_panel": "Sync-Quellen",
"sources_hint": "Sync-Quellen definieren IMAP-Server (mit ihrer Authentifizierungsmethode), die von Sync-Jobs wiederverwendet werden können. Definiere jede Quelle einmal und wähle sie dann beim Anlegen eines Sync-Jobs aus dem Dropdown aus.",
"source_add": "Quelle hinzufügen",
"source": "Quelle",
"source_name": "Name",
"source_description": "Beschreibung",
"source_scope": "Sichtbarkeit",
"source_scope_all": "Alle Nutzer",
"source_scope_domain": "Bestimmte Domains",
"source_scope_user": "Bestimmte Nutzer",
"source_domains": "Domains",
"source_users": "Nutzer",
"source_select": "Quelle wählen",
"source_auth_type": "Authentifizierung",
"source_oauth_block": "OAuth2-Konfiguration",
"source_token_status": "Token gültig bis",
"source_token_error": "Letzter Refresh-Fehler",
"source_refresh_now": "Token jetzt erneuern",
"source_secret_unchanged_hint": "Leer lassen, um das gespeicherte Secret beizubehalten.",
"oauth_source_hint": "Authentifizierung über OAuth2 — kein Passwort erforderlich.",
"source_oauth_flow": "OAuth2-Flow",
"source_oauth_flow_app": "App (Client Credentials)",
"source_oauth_flow_user": "Benutzer-Login (Authorization Code)",
"source_authorize_endpoint": "Authorize-Endpoint",
"source_userinfo_endpoint": "Userinfo-Endpoint",
"source_redirect_uri": "Redirect-URI",
"source_redirect_uri_hint": "Diese URI in der Provider-App (z.B. Azure) eintragen.",
"syncjob_oauth_connect": "Beim Provider anmelden",
"syncjob_oauth_connected": "Verbunden",
"sync_jobs": "Sync Jobs",
"syncjob_check_log": "Logs überprüfen",
"syncjob_last_run_result": "Letztes Ausführungsergebnis",
"syncjob_EX_OK": "Erfolg",
"syncjob_EXIT_CONNECTION_FAILURE": "Verbindungsproblem",
"syncjob_EXIT_TLS_FAILURE": "Problem mit verschlüsselter Verbindung",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentifizierungsproblem",
"syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit",
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Falscher Benutzername oder Passwort",
"add": "Sync-Job hinzufügen",
"add_hint": "Passwörter werden unverschlüsselt abgelegt!",
"edit": "Sync-Job bearbeiten",
"create": "Neuen Sync-Job erstellen",
"dry": "Synchronisation simulieren",
"subfolder2": "Ziel-Ordner<br><small>(leer = kein Unterordner)</small>",
"mins_interval": "Abrufintervall (Minuten)",
"maxage": "Maximales Alter in Tagen einer Nachricht, die kopiert werden soll<br><small>(0 = alle Nachrichten kopieren)</small>",
"exclude": "Elemente ausschließen (Regex)",
"automap": "Ordner automatisch mappen (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"delete1": "Lösche Nachricht nach Übertragung vom Quell-Server.",
"delete2": "Lösche Nachrichten von Ziel-Server, die nicht auf dem Quell-Server vorhanden sind.",
"delete2duplicates": "Lösche Duplikate im Ziel",
"skipcrossduplicates": "Duplikate auch über Ordner hinweg überspringen (\"first come, first serve\")",
"subscribeall": "Alle synchronisierten Ordner abonnieren",
"timeout1": "Timeout für Verbindung zum Remote-Host",
"timeout2": "Timeout für Verbindung zum lokalen Host",
"maxbytespersecond": "Max. Übertragungsrate in Bytes/s (0 für unlimitiert)",
"custom_params": "Eigene Parameter",
"custom_params_hint": "Richtig: --param=xy, falsch: --param xy"
},
"user": {
"action": "Aktion",
"active": "Aktiv",
@@ -1314,6 +1272,7 @@
"clear_recent_successful_connections": "Alle erfolgreichen Verbindungen bereinigen",
"client_configuration": "Konfigurationsanleitungen für E-Mail-Programme und Smartphones anzeigen",
"create_app_passwd": "Erstelle App-Passwort",
"create_syncjob": "Neuen Sync-Job erstellen",
"created_on": "Erstellt am",
"daily": "Täglich",
"day": "Tag",
@@ -1421,6 +1380,7 @@
"spamfilter_wl_desc": "Für E-Mail-Adressen, die vom Spamfilter <b>nicht</b> erfasst werden sollen. Die Verwendung von Wildcards ist gestattet. Ein Filter funktioniert lediglich für direkte Nicht-„Catch-All“-Alias-Adressen (Alias-Adressen mit lediglich einer Mailbox als Ziel-Adresse) sowie die Mailbox-Adresse selbst.",
"spamfilter_yellow": "Gelb: Die Nachricht ist vielleicht Spam, wird als Spam markiert und in den Junk-Ordner verschoben",
"status": "Status",
"sync_jobs": "Sync Jobs",
"tag_handling": "Umgang mit getaggten E-Mails steuern",
"tag_help_example": "Beispiel für eine getaggte E-Mail-Adresse: ich<b>+Facebook</b>@example.org",
"tag_help_explain": "Als Unterordner: Es wird ein Ordner mit dem Namen des Tags unterhalb der Inbox erstellt (\"INBOX/Facebook\").<br>\r\nIn Betreff: Der Name des Tags wird dem Betreff angefügt, etwa \"[Facebook] Meine Neuigkeiten\".",
@@ -1445,7 +1405,16 @@
"with_app_password": "mit App-Passwort",
"year": "Jahr",
"years": "Jahren",
"syncjob_EX_OK": "Erfolg",
"open_logs": "Öffne Logs",
"syncjob_check_log": "Logs überprüfen",
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Kann keine Verbindung zum Zielserver herstellen",
"syncjob_EXIT_OVERQUOTA": "Ziel Mailbox ist über dem Limit",
"syncjob_last_run_result": "Letztes Ausführungsergebnis",
"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",
"pushover_sound": "Ton"
},
"warning": {
+52 -82
View File
@@ -44,16 +44,24 @@
"app_name": "App name",
"app_password": "Add app password",
"app_passwd_protocols": "Allowed protocols for app password",
"automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"backup_mx_options": "Relay options",
"bcc_dest_format": "BCC destination must be a single valid email address.<br>If you need to send a copy to multiple addresses, create an alias and use it here.",
"comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview",
"custom_params": "Custom parameters",
"custom_params_hint": "Right: --param=xy, wrong: --param xy",
"delete1": "Delete from source when completed",
"delete2": "Delete messages on destination that are not on source",
"delete2duplicates": "Delete duplicates on destination",
"description": "Description",
"destination": "Destination",
"disable_login": "Disallow login (incoming mail is still accepted)",
"domain": "Domain",
"domain_matches_hostname": "Domain %s matches hostname",
"domain_quota_m": "Total domain quota (MiB)",
"dry": "Simulate synchronization",
"enc_method": "Encryption method",
"exclude": "Exclude objects (regex)",
"full_name": "Full name",
"gal": "Global Address List",
"gal_info": "The GAL contains all objects of a domain and cannot be edited by any user. Free/busy information in SOGo is missing, if disabled! <b>Restart SOGo to apply changes.</b>",
@@ -72,6 +80,7 @@
"mailbox_username": "Username (left part of an email address)",
"max_aliases": "Max. possible aliases",
"max_mailboxes": "Max. possible mailboxes",
"mins_interval": "Polling interval (minutes)",
"multiple_bookings": "Multiple bookings",
"nexthop": "Next hop",
"password": "Password",
@@ -91,10 +100,16 @@
"select_domain": "Please select a domain first",
"sieve_desc": "Short description",
"sieve_type": "Filter type",
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
"subscribeall": "Subscribe all folders",
"syncjob": "Add sync job",
"syncjob_hint": "Be aware that passwords need to be saved plain-text!",
"tags": "Tags",
"target_address": "Goto addresses",
"target_address_info": "<small>Full email address/es (comma-separated).</small>",
"target_domain": "Target domain",
"timeout1": "Timeout for connection to remote host",
"timeout2": "Timeout for connection to local host",
"username": "Username",
"validate": "Validate",
"validation_success": "Validated successfully"
@@ -544,22 +559,7 @@
"validity_missing": "Please assign a period of validity",
"value_missing": "Please provide all values",
"version_invalid": "Version %s is invalid",
"yotp_verification_failed": "Yubico OTP verification failed: %s",
"imapsync_source_in_use": "Sync source %s is still used by a sync job and cannot be deleted",
"imapsync_source_token_refresh_failed": "OAuth token refresh for %s failed",
"imapsync_source_scope_empty": "Select at least one domain for the chosen visibility",
"imapsync_source_unavailable": "The selected sync source is unavailable",
"imapsync_source_oauth_connect_required": "Sign in with the provider before saving this sync job",
"imapsync_source_name_invalid": "Invalid sync source name",
"imapsync_source_host_invalid": "Invalid host",
"imapsync_source_port_invalid": "Invalid port",
"imapsync_source_enc_invalid": "Invalid encryption method",
"imapsync_source_auth_invalid": "Invalid authentication method",
"imapsync_source_oauth_required": "OAuth2 requires token endpoint, client ID, client secret and scope",
"imapsync_source_oauth_endpoint_invalid": "Invalid OAuth2 endpoint URL",
"imapsync_source_oauth_extra_invalid": "OAuth2 extra parameters must be valid JSON",
"imapsync_source_invalid": "Invalid sync source data",
"imapsync_source_oauth_invalid": "Invalid OAuth2 configuration"
"yotp_verification_failed": "Yubico OTP verification failed: %s"
},
"datatables": {
"collapse_all": "Collapse All",
@@ -648,6 +648,7 @@
"app_name": "App name",
"app_passwd": "App password",
"app_passwd_protocols": "Allowed protocols for app password",
"automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"backup_mx_options": "Relay options",
"bcc_dest_format": "BCC destination must be a single valid email address.<br>If you need to send a copy to multiple addresses, create an alias and use it here.",
"client_id": "Client ID",
@@ -655,6 +656,9 @@
"comment_info": "A private comment is not visible to the user, while a public comment is shown as tooltip when hovering it in a user's overview",
"created_on": "Created on",
"custom_attributes": "Custom attributes",
"delete1": "Delete from source when completed",
"delete2": "Delete messages on destination that are not on source",
"delete2duplicates": "Delete duplicates on destination",
"delete_ays": "Please confirm the deletion process.",
"description": "Description",
"disable_login": "Disallow login (incoming mail is still accepted)",
@@ -678,6 +682,7 @@
"dont_check_sender_acl": "Disable sender check for domain %s (+ alias domains)",
"edit_alias_domain": "Edit Alias domain",
"encryption": "Encryption",
"exclude": "Exclude objects (regex)",
"extended_sender_acl": "External sender addresses",
"extended_sender_acl_info": "A DKIM domain key should be imported, if available.<br>\r\n Remember to add this server to the corresponding SPF TXT record.<br>\r\n Whenever a domain or alias domain is added to this server, that overlaps with an external address, the external address is removed.<br>\r\n Use @domain.tld to allow to send as *@domain.tld.",
"force_pw_update": "Force password update at next login",
@@ -708,7 +713,10 @@
"max_aliases": "Max. aliases",
"max_mailboxes": "Max. possible mailboxes",
"max_quota": "Max. quota per mailbox (MiB)",
"maxage": "Maximum age of messages in days that will be polled from remote<br><small>(0 = ignore age)</small>",
"maxbytespersecond": "Max. bytes per second <br><small>(0 = unlimited)</small>",
"mbox_rl_info": "This rate limit is applied on the SASL login name, it matches any \"from\" address used by the logged-in user. A mailbox rate limit overrides a domain-wide rate limit.",
"mins_interval": "Interval (min)",
"mta_sts": "MTA-STS",
"mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> is a standard that enforces email delivery between mail servers to use TLS with valid certificates. <br>It is used when <a target='_blank' href='https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> is not possible due to missing or unsupported DNSSEC.<br><b>Note</b>: If the receiving domain supports DANE with DNSSEC, DANE is <b>always</b> preferred MTA-STS only acts as a fallback.",
"mta_sts_version": "Version",
@@ -760,6 +768,7 @@
"sender_acl_info": "If mailbox user A is allowed to send as mailbox user B, the sender address is not automatically displayed as selectable \"from\" field in SOGo.<br>\r\n Mailbox user B needs to create a delegation in SOGo to allow mailbox user A to select their address as sender. To delegate a mailbox in SOGo, use the menu (three dots) to the right of your mailbox name in the upper left while in mail view. This behaviour does not apply to alias addresses.",
"sieve_desc": "Short description",
"sieve_type": "Filter type",
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
"sogo_access": "Direct forwarding to SOGo",
"sogo_access_info": "After logging in, the user is automatically redirected to SOGo.",
"sogo_visible": "Alias is visible in SOGo",
@@ -768,8 +777,12 @@
"spam_filter": "Spam filter",
"spam_policy": "Add or remove items to allow-/denylist",
"spam_score": "Set a custom spam score",
"subfolder2": "Sync into subfolder on destination<br><small>(empty = do not use subfolder)</small>",
"syncjob": "Edit sync job",
"target_address": "Goto address/es <small>(comma-separated)</small>",
"target_domain": "Target domain",
"timeout1": "Timeout for connection to remote host",
"timeout2": "Timeout for connection to local host",
"title": "Edit object",
"unchanged_if_empty": "If unchanged leave blank",
"username": "Username",
@@ -916,6 +929,7 @@
"filters": "Filters",
"fname": "Full name",
"force_pw_update": "Force password update at next login",
"force_tfa": "TFA",
"gal": "Global Address List",
"goto_ham": "Learn as <b>ham</b>",
"goto_spam": "Learn as <b>spam</b>",
@@ -990,6 +1004,16 @@
"spam_aliases": "Temp. alias",
"stats": "Statistics",
"status": "Status",
"sync_jobs": "Sync jobs",
"syncjob_check_log": "Check log",
"syncjob_last_run_result": "Last run result",
"syncjob_EX_OK": "Success",
"syncjob_EXIT_CONNECTION_FAILURE": "Connection problem",
"syncjob_EXIT_TLS_FAILURE": "Problem with encrypted connection",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentication problem",
"syncjob_EXIT_OVERQUOTA": "Target mailbox is over quota",
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Can't connect to remote server",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Wrong username or password",
"table_size": "Table size",
"table_size_show_n": "Show %s items",
"target_address": "Goto address",
@@ -1191,11 +1215,7 @@
"verified_fido2_login": "Verified FIDO2 login",
"verified_totp_login": "Verified TOTP login",
"verified_webauthn_login": "Verified WebAuthn login",
"verified_yotp_login": "Verified Yubico OTP login",
"imapsync_source_added": "Sync source %s has been added",
"imapsync_source_modified": "Sync source %s has been saved",
"imapsync_source_deleted": "Sync source %s has been deleted",
"imapsync_source_token_refreshed": "OAuth token for %s has been refreshed"
"verified_yotp_login": "Verified Yubico OTP login"
},
"tfa": {
"authenticators": "Authenticators",
@@ -1230,67 +1250,6 @@
"waiting_usb_register": "<i>Waiting for USB device...</i><br><br>Please enter your password above and confirm your registration by tapping the button on your USB device.",
"yubi_otp": "Yubico OTP authentication"
},
"syncjobs": {
"sources_panel": "Sync Sources",
"sources_hint": "Sync sources define IMAP servers (and their authentication method) that sync jobs can reuse. Define each source once, then pick it from the dropdown when creating sync jobs.",
"source_add": "Add source",
"source": "Source",
"source_name": "Name",
"source_description": "Description",
"source_scope": "Visibility",
"source_scope_all": "All users",
"source_scope_domain": "Specific domains",
"source_scope_user": "Specific users",
"source_domains": "Domains",
"source_users": "Users",
"source_select": "Select source",
"source_auth_type": "Authentication",
"source_oauth_block": "OAuth2 configuration",
"source_token_status": "Token valid until",
"source_token_error": "Last refresh error",
"source_refresh_now": "Refresh token now",
"source_secret_unchanged_hint": "Leave blank to keep the stored secret unchanged.",
"oauth_source_hint": "Authentication via OAuth2 — no password required.",
"source_oauth_flow": "OAuth2 flow",
"source_oauth_flow_app": "App (client credentials)",
"source_oauth_flow_user": "User login (authorization code)",
"source_authorize_endpoint": "Authorize endpoint",
"source_userinfo_endpoint": "Userinfo endpoint",
"source_redirect_uri": "Redirect URI",
"source_redirect_uri_hint": "Register this URI in the provider app (e.g. Azure).",
"syncjob_oauth_connect": "Sign in with the provider",
"syncjob_oauth_connected": "Connected",
"sync_jobs": "Sync jobs",
"syncjob_check_log": "Check log",
"syncjob_last_run_result": "Last run result",
"syncjob_EX_OK": "Success",
"syncjob_EXIT_CONNECTION_FAILURE": "Connection problem",
"syncjob_EXIT_TLS_FAILURE": "Problem with encrypted connection",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentication problem",
"syncjob_EXIT_OVERQUOTA": "Target mailbox is over quota",
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Can't connect to remote server",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Wrong username or password",
"add": "Add sync job",
"add_hint": "Be aware that passwords need to be saved plain-text!",
"edit": "Edit sync job",
"create": "Create new sync job",
"dry": "Simulate synchronization",
"subfolder2": "Sync into subfolder on destination<br><small>(empty = do not use subfolder)</small>",
"mins_interval": "Polling interval (minutes)",
"maxage": "Maximum age of messages in days that will be polled from remote<br><small>(0 = ignore age)</small>",
"exclude": "Exclude objects (regex)",
"automap": "Try to automap folders (\"Sent items\", \"Sent\" => \"Sent\" etc.)",
"delete1": "Delete from source when completed",
"delete2": "Delete messages on destination that are not on source",
"delete2duplicates": "Delete duplicates on destination",
"skipcrossduplicates": "Skip duplicate messages across folders (first come, first serve)",
"subscribeall": "Subscribe all folders",
"timeout1": "Timeout for connection to remote host",
"timeout2": "Timeout for connection to local host",
"maxbytespersecond": "Max. bytes per second <br><small>(0 = unlimited)</small>",
"custom_params": "Custom parameters",
"custom_params_hint": "Right: --param=xy, wrong: --param xy"
},
"user": {
"action": "Action",
"active": "Active",
@@ -1321,6 +1280,7 @@
"clear_recent_successful_connections": "Clear seen successful connections",
"client_configuration": "Show configuration guides for email clients and smartphones",
"create_app_passwd": "Create app password",
"create_syncjob": "Create new sync job",
"created_on": "Created on",
"daily": "Daily",
"day": "day",
@@ -1430,6 +1390,16 @@
"spamfilter_wl_desc": "Allowlisted email addresses are programmed to <b>never</b> classify as spam. Wildcards may be used. A filter is only applied to direct aliases (aliases with a single target mailbox) excluding catch-all aliases and a mailbox itself.",
"spamfilter_yellow": "Yellow: this message may be spam, will be tagged as spam and moved to your junk folder",
"status": "Status",
"sync_jobs": "Sync jobs",
"syncjob_check_log": "Check log",
"syncjob_last_run_result": "Last run result",
"syncjob_EX_OK": "Success",
"syncjob_EXIT_CONNECTION_FAILURE": "Connection problem",
"syncjob_EXIT_TLS_FAILURE": "Problem with encrypted connection",
"syncjob_EXIT_AUTHENTICATION_FAILURE": "Authentication problem",
"syncjob_EXIT_OVERQUOTA": "Target mailbox is over quota",
"syncjob_EXIT_CONNECTION_FAILURE_HOST1": "Can't connect to remote server",
"syncjob_EXIT_AUTHENTICATION_FAILURE_USER1": "Wrong username or password",
"tag_handling": "Set handling for tagged mail",
"tag_help_example": "Example for a tagged email address: me<b>+Facebook</b>@example.org",
"tag_help_explain": "In subfolder: a new subfolder named after the tag will be created below INBOX (\"INBOX/Facebook\").<br>\r\nIn subject: the tags name will be prepended to the mails subject, example: \"[Facebook] My News\".",
+17 -6
View File
@@ -111,7 +111,8 @@
"app_passwd_protocols": "Protocoles autorisés pour le mot de passe de l'application",
"dry": "Simuler la synchronisation",
"internal": "Interne",
"internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine."
"internal_info": "Les alias internes sont accessibles uniquement depuis le domaine ou les alias du domaine.",
"sender_allowed": "Autoriser l'envoi sous cet alias"
},
"admin": {
"access": "Accès",
@@ -556,7 +557,9 @@
"max_age_invalid": "L'âge maximum %s est invalide",
"mode_invalid": "Le mode %s est invalide",
"mx_invalid": "L'enregistrement MX %s est invalide",
"version_invalid": "La version %s est invalide"
"version_invalid": "La version %s est invalide",
"tfa_removal_blocked": "Lauthentification à deux facteurs ne peut pas être supprimée, car elle est requise pour votre compte.",
"quarantine_category_invalid": "La catégorie de quarantaine doit être lune des suivantes : add_header, reject, all\""
},
"debug": {
"chart_this_server": "Graphique (ce serveur)",
@@ -755,7 +758,9 @@
"mta_sts_info": "<a href='https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol#SMTP_MTA_Strict_Transport_Security' target='_blank'>MTA-STS</a> est un standard qui oblige la délivrance des courriels entre les serveurs de courriels à utiliser TLS avec des certificats valides. <br>Il est utilisé quand <a target='_blank' href='https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities'>DANE</a> n'est pas possible à cause d'un manque ou d'un non support de DNSSEC.<br><b>Note</b> : Si le domaine du destinataire supporte DANE avec DNSSEC, DANE est <b>toujours</b> préféré MTA-STS sert seulement en secours.",
"mta_sts_mode_info": "Il y a trois modes parmi lesquels choisir :<ul><li><em>testing</em> la politique est seulement surveillée, les violations n'ont pas d'impact.</li><li><em>enforce</em> la politique est appliquée strictement, les connexions sans TLS valide sont rejetées.</li><li><em>none</em> la politique est publiée mais non appliquée.</li></ul>",
"mta_sts_max_age_info": "Durée en secondes pendant laquelle les serveurs de courriel peuvent mettre en cache cette politique avant de revérifier.",
"mta_sts_mx_info": "Autoriser l'envoi uniquement aux noms d'hôtes des serveurs de courriels indiqués explicitement ; le MTA émetteur vérifie si le nom d'hôte DNS du MX correspond à la liste de la politique, et autorise la délivrance seulement avec un certificat TLS valide (protège contre le MITM)."
"mta_sts_mx_info": "Autoriser l'envoi uniquement aux noms d'hôtes des serveurs de courriels indiqués explicitement ; le MTA émetteur vérifie si le nom d'hôte DNS du MX correspond à la liste de la politique, et autorise la délivrance seulement avec un certificat TLS valide (protège contre le MITM).",
"sender_allowed": "Autoriser l'envoi sous cet alias",
"sender_allowed_info": "Si cette option est désactivée, cet alias peut uniquement recevoir des courriels. Utilisez les ACL dexpéditeur pour autoriser certaines boîtes aux lettres à envoyer des messages via cet alias."
},
"footer": {
"cancel": "Annuler",
@@ -988,7 +993,8 @@
"recipient": "Destinataire",
"open_logs": "Afficher les journaux",
"iam": "Fournisseur d'identité",
"internal": "Interne"
"internal": "Interne",
"force_tfa": "TFA"
},
"oauth2": {
"access_denied": "Veuillez vous connecter en tant que propriétaire de la boîte de réception pour accorder laccès via Oauth2.",
@@ -1189,7 +1195,11 @@
"yubi_otp": "Authentification OTP Yubico",
"authenticators": "Authentificateurs",
"u2f_deprecated_important": "Veuillez enregistrer votre clé dans le panneau d'administration avec la nouvelle méthode WebAuthn.",
"u2f_deprecated": "Il semble que votre clé ait été enregistrée à l'aide de la méthode U2F obsolète. Nous allons désactiver l'authentification à deux facteurs pour vous et supprimer votre clé."
"u2f_deprecated": "Il semble que votre clé ait été enregistrée à l'aide de la méthode U2F obsolète. Nous allons désactiver l'authentification à deux facteurs pour vous et supprimer votre clé.",
"force_tfa": "Imposer l'authentification à deux facteurs lors de la connexion",
"force_tfa_info": "Lutilisateur doit configurer lauthentification à deux facteurs avant de pouvoir accéder à linterface.",
"setup_title": "Authentification à deux facteurs requise",
"setup_required": "Votre compte nécessite lauthentification à deux facteurs. Veuillez configurer une méthode 2FA pour continuer."
},
"fido2": {
"set_fn": "Définir un nom",
@@ -1378,7 +1388,8 @@
"forever": "Pour toujours",
"spam_aliases_info": "Un alias de spam est une adresse de courriel temporaire qui peut être utilisée pour protéger les véritables adresses de courriel. <br> De manière optionnelle, une durée d'expiration peut être définie afin que l'alias soit automatiquement désactivé après la période définie, éliminant ainsi les adresses étant abusées ou ayant fuité.",
"authentication": "Authentification",
"protocols": "Protocoles"
"protocols": "Protocoles",
"pw_update_required": "Votre compte nécessite un changement de mot de passe. Veuillez définir un nouveau mot de passe pour continuer."
},
"warning": {
"cannot_delete_self": "Impossible de supprimer lutilisateur connecté",
+2 -1
View File
@@ -1001,7 +1001,8 @@
"weekly": "Tedensko",
"sieve_info": "Na uporabnika lahko shranite več filtrov, vendar je lahko hkrati aktiven le en predfilter in en postfilter.<br>\nVsak filter bo obdelan v opisanem vrstnem redu. Niti neuspešen skript niti izdan ukaz »keep;« ne bosta ustavila obdelave nadaljnjih skript. Spremembe globalnih skriptov sita bodo sprožile ponovni zagon Dovecota.<br><br>Globalni predfilter sita &#8226; Predfilter &#8226; Uporabniški skripti &#8226; Postfilter &#8226; Globalni postfilter sita",
"tls_policy_maps_info": "Ta preslikava pravilnikov preglasi pravila odhodnega prenosa TLS neodvisno od uporabnikovih nastavitev pravilnikov TLS.<br>\n Za več informacij preverite <a href=\"http://www.postfix.org/postconf.5.html#smtp_tls_policy_maps\" target=\"_blank\">dokumentacijo »smtp_tls_policy_maps«</a>.",
"internal": "Notranje"
"internal": "Notranje",
"force_tfa": "TFA"
},
"fido2": {
"known_ids": "Znani ID-ji",
+1
View File
@@ -65,6 +65,7 @@ if (isset($_GET['app_password'])) {
$attr['protocols'][] = 'dav_access';
}
app_passwd("add", $attr);
$password = htmlspecialchars($password, ENT_NOQUOTES);
} else {
$app_password = false;
}
-85
View File
@@ -1,85 +0,0 @@
<?php
// Per-user OAuth2 authorization_code flow for IMAP sync sources.
// Opened as a popup from the sync-job modal:
// ?action=start&source_id=N -> redirect the logged-in user to the provider consent screen
// (callback, ?code&state) -> exchange code, store the per-user token, message the opener
// The resulting token is bound to the authenticated remote identity, which fixes syncjob user1.
require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/prerequisites.inc.php';
function syncjob_oauth_redirect_uri() {
// Must match the redirect URI registered at the provider.
return 'https://' . $_SERVER['HTTP_HOST'] . '/syncjob-oauth';
}
function syncjob_oauth_finish($ok, $payload) {
// Renders a tiny page that hands the result back to the opener modal and closes the popup.
header('Content-Type: text/html; charset=utf-8');
$json = json_encode(array_merge(array('type' => 'syncjob_oauth', 'ok' => $ok), $payload));
echo '<!doctype html><meta charset="utf-8"><title>OAuth</title><body><script>'
. 'try { if (window.opener) window.opener.postMessage(' . $json . ', window.location.origin); } catch (e) {}'
. 'window.close();'
. 'document.body.innerText = ' . json_encode($ok ? 'Connected. You can close this window.' : 'Authentication failed. You can close this window.') . ';'
. '</script></body>';
exit;
}
// Must be an authenticated session; users additionally need the syncjobs ACL.
if (empty($_SESSION['mailcow_cc_username']) || empty($_SESSION['mailcow_cc_role'])) {
header('Location: /');
exit;
}
if ($_SESSION['mailcow_cc_role'] == 'user' && ($_SESSION['acl']['syncjobs'] ?? '0') != '1') {
syncjob_oauth_finish(false, array('error' => 'access_denied'));
}
$action = isset($_GET['action']) ? $_GET['action'] : ((isset($_GET['code']) || isset($_GET['error'])) ? 'callback' : '');
if ($action == 'start') {
$source_id = intval($_GET['source_id'] ?? 0);
// get/source enforces visibility for the current session
$source = syncjob('get', 'source', array('id' => $source_id, 'with_secret' => true));
if (!$source || $source['auth_type'] != 'XOAUTH2' || $source['oauth_flow'] != 'authorization_code') {
syncjob_oauth_finish(false, array('error' => 'source_unavailable'));
}
$state = bin2hex(random_bytes(16));
$_SESSION['syncjob_oauth2state'] = $state;
$_SESSION['syncjob_oauth_source_id'] = $source_id;
header('Location: ' . imapsync_source_oauth_authorize_url($source, syncjob_oauth_redirect_uri(), $state));
exit;
}
// --- callback ---
if (isset($_GET['error'])) {
syncjob_oauth_finish(false, array('error' => substr((string)$_GET['error'], 0, 200)));
}
// CSRF: state must match the one stored at 'start'
if (empty($_GET['code']) || empty($_GET['state']) || !hash_equals((string)($_SESSION['syncjob_oauth2state'] ?? ''), (string)$_GET['state'])) {
syncjob_oauth_finish(false, array('error' => 'invalid_state'));
}
$source_id = intval($_SESSION['syncjob_oauth_source_id'] ?? 0);
unset($_SESSION['syncjob_oauth2state'], $_SESSION['syncjob_oauth_source_id']);
$source = syncjob('get', 'source', array('id' => $source_id, 'with_secret' => true));
if (!$source || $source['auth_type'] != 'XOAUTH2' || $source['oauth_flow'] != 'authorization_code') {
syncjob_oauth_finish(false, array('error' => 'source_unavailable'));
}
$token = imapsync_source_token_post($source['oauth_token_endpoint'], array(
'grant_type' => 'authorization_code',
'client_id' => $source['oauth_client_id'],
'client_secret' => $source['oauth_client_secret'],
'code' => $_GET['code'],
'redirect_uri' => syncjob_oauth_redirect_uri(),
'scope' => $source['oauth_scope'],
));
if (isset($token['error'])) {
syncjob_oauth_finish(false, array('error' => $token['error']));
}
$email = imapsync_source_oauth_identity($source, $token);
if (!$email) {
syncjob_oauth_finish(false, array('error' => 'no_identity'));
}
imapsync_source_store_user_token($source_id, $email, $token);
syncjob_oauth_finish(true, array('source_id' => $source_id, 'user1' => $email));
-1
View File
@@ -26,7 +26,6 @@
var lang_user = {{ lang_user|raw }};
var lang_admin = {{ lang_admin|raw }};
var lang_datatables = {{ lang_datatables|raw }};
var lang = {syncjobs: {{ lang_syncjobs|raw }}};
var csrf_token = '{{ csrf_token }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var table_for_domain = '{{ domain }}';
@@ -1,181 +0,0 @@
{% extends 'edit.twig' %}
{% block inner_content %}
{% if result %}
<h4 class="mb-4">{{ lang.syncjobs.source }}: {{ result.name }}</h4>
<form class="form-horizontal" data-id="edit_imapsync_source" role="form" method="post">
<input type="hidden" value="0" name="active">
<div class="row mb-2">
<label class="control-label col-sm-2" for="name">{{ lang.syncjobs.source_name }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="name" id="name" maxlength="100" value="{{ result.name }}" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="description">{{ lang.syncjobs.source_description }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="description" id="description" maxlength="255" value="{{ result.description }}">
</div>
</div>
{% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %}
<div class="row mb-2">
<label class="control-label col-sm-2" for="scope">{{ lang.syncjobs.source_scope }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-scope" name="scope" id="scope">
{% if mailcow_cc_role == 'admin' %}
<option value="all"{% if result.scope == 'all' %} selected{% endif %}>{{ lang.syncjobs.source_scope_all }}</option>
{% endif %}
<option value="domain"{% if result.scope == 'domain' %} selected{% endif %}>{{ lang.syncjobs.source_scope_domain }}</option>
<option value="user"{% if result.scope == 'user' %} selected{% endif %}>{{ lang.syncjobs.source_scope_user }}</option>
</select>
</div>
</div>
<div class="row mb-2 imapsync-source-domains-row" style="display:{% if result.scope == 'domain' %}flex{% else %}none{% endif %};">
<label class="control-label col-sm-2" for="domains">{{ lang.syncjobs.source_domains }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-domains" name="domains[]" data-live-search="true" title="{{ lang.syncjobs.source_domains }}" multiple>
{% for d in result.domains %}<option value="{{ d }}" selected>{{ d }}</option>{% endfor %}
</select>
</div>
</div>
<div class="row mb-2 imapsync-source-users-row" style="display:{% if result.scope == 'user' %}flex{% else %}none{% endif %};">
<label class="control-label col-sm-2" for="users">{{ lang.syncjobs.source_users }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-users" name="users[]" data-live-search="true" title="{{ lang.syncjobs.source_users }}" multiple>
{% for u in result.users %}<option value="{{ u }}" selected>{{ u }}</option>{% endfor %}
</select>
</div>
</div>
{% else %}
<p class="text-muted">{{ lang.syncjobs.source_scope }}: <code>{{ lang.syncjobs['source_scope_' ~ result.scope] }}</code></p>
{% endif %}
<div class="row mb-2">
<label class="control-label col-sm-2" for="host1">{{ lang.add.hostname }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="host1" id="host1" value="{{ result.host1 }}" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="port1">{{ lang.add.port }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="{{ result.port1 }}" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="enc1">{{ lang.add.enc_method }}</label>
<div class="col-sm-10">
<select name="enc1" id="enc1">
<option value="SSL"{% if result.enc1 == 'SSL' %} selected{% endif %}>SSL</option>
<option value="TLS"{% if result.enc1 == 'TLS' %} selected{% endif %}>STARTTLS</option>
<option value="PLAIN"{% if result.enc1 == 'PLAIN' %} selected{% endif %}>PLAIN</option>
</select>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="auth_type">{{ lang.syncjobs.source_auth_type }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-auth-type" name="auth_type" id="auth_type" required>
<option value="PLAIN"{% if result.auth_type == 'PLAIN' %} selected{% endif %}>PLAIN</option>
<option value="LOGIN"{% if result.auth_type == 'LOGIN' %} selected{% endif %}>LOGIN</option>
<option value="CRAM-MD5"{% if result.auth_type == 'CRAM-MD5' %} selected{% endif %}>CRAM-MD5</option>
<option value="XOAUTH2"{% if result.auth_type == 'XOAUTH2' %} selected{% endif %}>XOAUTH2 (OAuth2)</option>
</select>
</div>
</div>
<fieldset class="imapsync-source-oauth-block" style="display:{% if result.auth_type == 'XOAUTH2' %}block{% else %}none{% endif %};">
<legend class="fs-5">{{ lang.syncjobs.source_oauth_block }}</legend>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_flow">{{ lang.syncjobs.source_oauth_flow }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-oauth-flow" name="oauth_flow" id="oauth_flow">
<option value="client_credentials"{% if result.oauth_flow != 'authorization_code' %} selected{% endif %}>{{ lang.syncjobs.source_oauth_flow_app }}</option>
<option value="authorization_code"{% if result.oauth_flow == 'authorization_code' %} selected{% endif %}>{{ lang.syncjobs.source_oauth_flow_user }}</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_token_endpoint">Token-Endpoint</label>
<div class="col-sm-10">
<input type="url" class="form-control" name="oauth_token_endpoint" id="oauth_token_endpoint" value="{{ result.oauth_token_endpoint }}">
</div>
</div>
<div class="imapsync-source-authcode-block" style="display:{% if result.oauth_flow == 'authorization_code' %}block{% else %}none{% endif %};">
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_authorize_endpoint">{{ lang.syncjobs.source_authorize_endpoint }}</label>
<div class="col-sm-10">
<input type="url" class="form-control" name="oauth_authorize_endpoint" id="oauth_authorize_endpoint" value="{{ result.oauth_authorize_endpoint }}">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_userinfo_endpoint">{{ lang.syncjobs.source_userinfo_endpoint }}</label>
<div class="col-sm-10">
<input type="url" class="form-control" name="oauth_userinfo_endpoint" id="oauth_userinfo_endpoint" value="{{ result.oauth_userinfo_endpoint }}">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2">{{ lang.syncjobs.source_redirect_uri }}</label>
<div class="col-sm-10">
<input type="text" class="form-control imapsync-source-redirect-uri" readonly value="">
<small class="text-muted">{{ lang.syncjobs.source_redirect_uri_hint }}</small>
</div>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_client_id">Client-ID</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="oauth_client_id" id="oauth_client_id" value="{{ result.oauth_client_id }}">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_client_secret">Client-Secret</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="oauth_client_secret" id="oauth_client_secret" placeholder="••••••••">
<small class="text-muted">{{ lang.syncjobs.source_secret_unchanged_hint }}</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_scope">Scope</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="oauth_scope" id="oauth_scope" value="{{ result.oauth_scope }}">
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="oauth_extra_params">Extra-Params (JSON)</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="oauth_extra_params" id="oauth_extra_params" value="{{ result.oauth_extra_params }}">
</div>
</div>
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<p class="mb-1">
{{ lang.syncjobs.source_token_status }}:
{% if result.oauth_token_expires %}
<code>{{ result.oauth_token_expires|date('Y-m-d H:i:s') }}</code>
{% else %}
<span class="text-muted">—</span>
{% endif %}
</p>
{% if result.oauth_last_refresh_error %}
<p class="text-danger">{{ lang.syncjobs.source_token_error }}: <code>{{ result.oauth_last_refresh_error }}</code></p>
{% endif %}
<button type="button" class="btn btn-sm btn-secondary" data-action="edit_selected" data-id="edit_imapsync_source" data-item="{{ result.id }}" data-api-url='edit/syncjob_source/refresh_token' data-api-attr='{}'>{{ lang.syncjobs.source_refresh_now }}</button>
</div>
</div>
</fieldset>
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="active"{% if result.active == '1' %} checked{% endif %}> {{ lang.edit.active }}</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="edit_selected" data-id="edit_imapsync_source" data-item="{{ result.id }}" data-api-url='edit/syncjob_source' data-api-attr='{}' href="#">{{ lang.edit.save }}</button>
</div>
</div>
</form>
{% else %}
{{ parent() }}
{% endif %}
{% endblock %}
@@ -8,6 +8,7 @@
<input type="hidden" value="default" name="sender_acl">
<input type="hidden" value="0" name="force_pw_update">
<input type="hidden" value="0" name="force_tfa">
<input type="hidden" value="0" name="sogo_access">
<input type="hidden" value="0" name="protocol_access">
@@ -165,6 +166,14 @@
</div>
</div>
</div>
<div class="row">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="force_tfa" id="force_tfa"{% if template.attributes.force_tfa == '1' %} checked{% endif %}> {{ lang.tfa.force_tfa }}</label>
<small class="text-muted">{{ lang.tfa.force_tfa_info }}</small>
</div>
</div>
</div>
{% if not skip_sogo %}
<div class="row">
<div class="offset-sm-2 col-sm-10">
+37 -33
View File
@@ -2,7 +2,7 @@
{% block inner_content %}
{% if result %}
<h4 class="mb-4">{{ lang.syncjobs.edit }}</h4>
<h4 class="mb-4">{{ lang.edit.syncjob }}</h4>
<form class="form-horizontal" data-id="editsyncjob" role="form" method="post">
<input type="hidden" value="0" name="delete2duplicates">
<input type="hidden" value="0" name="delete1">
@@ -12,16 +12,16 @@
<input type="hidden" value="0" name="active">
<input type="hidden" value="0" name="subscribeall">
<input type="hidden" value="0" name="dry">
<div class="row mb-4">
<label class="control-label col-sm-2" for="source_id">{{ lang.syncjobs.source }}</label>
<div class="row mb-2">
<label class="control-label col-sm-2" for="host1">{{ lang.edit.hostname }}</label>
<div class="col-sm-10">
<select class="imapsync-source-select" name="source_id" id="source_id"
data-live-search="true"
data-current-source-id="{{ result.source_id }}"
data-current-source-auth-type="{{ result.source_auth_type }}" required>
<option value="{{ result.source_id }}" data-auth-type="{{ result.source_auth_type }}" data-oauth-flow="{{ result.source_oauth_flow }}" selected>{{ result.source_name }} ({{ result.source_host }}:{{ result.source_port }} / {{ result.source_auth_type }})</option>
</select>
<small class="text-muted oauth-source-hint" style="display:{% if result.source_auth_type == 'XOAUTH2' %}block{% else %}none{% endif %};">{{ lang.syncjobs.oauth_source_hint }}</small>
<input type="text" class="form-control" name="host1" id="host1" value="{{ result.host1 }}">
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="port1">Port</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="port1" id="port1" min="1" max="65535" value="{{ result.port1 }}">
</div>
</div>
<div class="row mb-2">
@@ -30,118 +30,122 @@
<input type="text" class="form-control" name="user1" id="user1" value="{{ result.user1 }}">
</div>
</div>
<div class="row mb-2 imapsync-oauth-connect-row" style="display:{% if result.source_oauth_flow == 'authorization_code' %}flex{% else %}none{% endif %};">
<div class="offset-sm-2 col-sm-10">
<button type="button" class="btn btn-sm btn-primary imapsync-oauth-connect-btn">{{ lang.syncjobs.syncjob_oauth_connect }}</button>
<span class="imapsync-oauth-connect-status ms-2"></span>
</div>
</div>
<div class="row mb-4 password1-row" style="display:{% if result.source_auth_type == 'XOAUTH2' %}none{% else %}flex{% endif %};">
<div class="row mb-4">
<label class="control-label col-sm-2" for="password1">{{ lang.edit.password }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="password1" id="password1" value="{{ result.password1 }}">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="mins_interval">{{ lang.syncjobs.mins_interval }}</label>
<label class="control-label col-sm-2" for="enc1">{{ lang.edit.encryption }}</label>
<div class="col-sm-10">
<select id="enc1" name="enc1">
<option value="SSL"{% if result.enc1 == 'SSL' %} selected{% endif %}>SSL</option>
<option value="TLS"{% if result.enc1 == 'TLS' %} selected{% endif %}>STARTTLS</option>
<option value="PLAIN"{% if result.enc1 == 'PLAIN' %} selected{% endif %}>PLAIN</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="mins_interval">{{ lang.edit.mins_interval }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="mins_interval" min="1" max="43800" value="{{ result.mins_interval }}" required>
<small class="text-muted">1-43800</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="subfolder2">{{ lang.syncjobs.subfolder2|raw }}</label>
<label class="control-label col-sm-2" for="subfolder2">{{ lang.edit.subfolder2|raw }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="subfolder2" id="subfolder2" value="{{ result.subfolder2 }}">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="maxage">{{ lang.syncjobs.maxage|raw }}</label>
<label class="control-label col-sm-2" for="maxage">{{ lang.edit.maxage|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxage" id="maxage" min="0" max="32000" value="{{ result.maxage }}">
<small class="text-muted">0-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="maxbytespersecond">{{ lang.syncjobs.maxbytespersecond|raw }}</label>
<label class="control-label col-sm-2" for="maxbytespersecond">{{ lang.edit.maxbytespersecond|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxbytespersecond" id="maxbytespersecond" min="0" max="125000000" value="{{ result.maxbytespersecond }}">
<small class="text-muted">0-125000000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="timeout1">{{ lang.syncjobs.timeout1 }}</label>
<label class="control-label col-sm-2" for="timeout1">{{ lang.add.timeout1 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout1" id="timeout1" min="1" max="32000" value="{{ result.timeout1 }}">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="timeout2">{{ lang.syncjobs.timeout2 }}</label>
<label class="control-label col-sm-2" for="timeout2">{{ lang.add.timeout2 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout2" id="timeout2" min="1" max="32000" value="{{ result.timeout2 }}">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="exclude">{{ lang.syncjobs.exclude }}</label>
<label class="control-label col-sm-2" for="exclude">{{ lang.edit.exclude }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="exclude" id="exclude" value="{{ result.exclude }}">
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="custom_params">{{ lang.syncjobs.custom_params }}</label>
<label class="control-label col-sm-2" for="custom_params">{{ lang.add.custom_params }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="custom_params" id="custom_params" value="{{ result.custom_params }}" placeholder="--some-param=xy --other-param=yx">
<small class="text-muted">{{ lang.syncjobs.custom_params_hint }}</small>
<small class="text-muted">{{ lang.add.custom_params_hint }}</small>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates"{% if result.delete2duplicates == '1' %} checked{% endif %}> {{ lang.syncjobs.delete2duplicates }} (--delete2duplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates"{% if result.delete2duplicates == '1' %} checked{% endif %}> {{ lang.edit.delete2duplicates }} (--delete2duplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"{% if result.delete1 == '1' %} checked{% endif %}> {{ lang.syncjobs.delete1 }} (--delete1)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"{% if result.delete1 == '1' %} checked{% endif %}> {{ lang.edit.delete1 }} (--delete1)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"{% if result.delete2 == '1' %} checked{% endif %}> {{ lang.syncjobs.delete2 }} (--delete2)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"{% if result.delete2 == '1' %} checked{% endif %}> {{ lang.edit.delete2 }} (--delete2)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="automap"{% if result.automap == '1' %} checked{% endif %}> {{ lang.syncjobs.automap }} (--automap)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="automap"{% if result.automap == '1' %} checked{% endif %}> {{ lang.edit.automap }} (--automap)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"{% if result.skipcrossduplicates == '1' %} checked{% endif %}> {{ lang.syncjobs.skipcrossduplicates }} (--skipcrossduplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"{% if result.skipcrossduplicates == '1' %} checked{% endif %}> {{ lang.edit.skipcrossduplicates }} (--skipcrossduplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall"{% if result.subscribeall == '1' %} checked{% endif %}> {{ lang.syncjobs.subscribeall }} (--subscribeall)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall"{% if result.subscribeall == '1' %} checked{% endif %}> {{ lang.add.subscribeall }} (--subscribeall)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="dry"{% if result.dry == '1' %} checked{% endif %}> {{ lang.syncjobs.dry }} (--dry)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="dry"{% if result.dry == '1' %} checked{% endif %}> {{ lang.add.dry }} (--dry)</label>
</div>
</div>
</div>
+1 -2
View File
@@ -33,7 +33,7 @@
<li role="presentation"><button class="dropdown-item" aria-selected="false" aria-controls="tab-domain-aliases" role="tab" data-bs-toggle="tab" data-bs-target="#tab-domain-aliases">{{ lang.mailbox.domain_aliases }}</button></li>
</ul>
</li>
<li class="nav-item" role="presentation"><button class="nav-link" aria-selected="false" aria-controls="tab-syncjobs" role="tab" data-bs-toggle="tab" data-bs-target="#tab-syncjobs">{{ lang.syncjobs.sync_jobs }}</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" aria-selected="false" aria-controls="tab-syncjobs" role="tab" data-bs-toggle="tab" data-bs-target="#tab-syncjobs">{{ lang.mailbox.sync_jobs }}</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" aria-selected="false" aria-controls="tab-filters" role="tab" data-bs-toggle="tab" data-bs-target="#tab-filters">{{ lang.mailbox.filters }}</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" aria-selected="false" aria-controls="tab-bcc" role="tab" data-bs-toggle="tab" data-bs-target="#tab-bcc">{{ lang.mailbox.address_rewriting }}</button></li>
<li class="nav-item" role="presentation"{% if mailcow_cc_role != 'admin' %} class="d-none"{% endif %}><button class="nav-link" aria-selected="false" aria-controls="tab-tls-policy" role="tab" data-bs-toggle="tab" data-bs-target="#tab-tls-policy">{{ lang.mailbox.tls_policy_maps }}</button></li>
@@ -67,7 +67,6 @@
<script type='text/javascript'>
var acl = '{{ acl_json|raw }}';
var lang = {{ lang_mailbox|raw }};
lang.syncjobs = {{ lang_syncjobs|raw }};
var lang_rl = {{ lang_rl|raw }};
var lang_edit = {{ lang_edit|raw }};
var lang_datatables = {{ lang_datatables|raw }};
+4 -29
View File
@@ -1,35 +1,10 @@
<div class="tab-pane fade" id="tab-syncjobs" role="tabpanel" aria-labelledby="tab-syncjobs">
<div class="card mb-4">
<div class="card-header d-flex fs-5">
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-imapsync-sources" data-bs-toggle="collapse" aria-controls="collapse-tab-imapsync-sources">
{{ lang.syncjobs.sources_panel }} <span class="badge bg-info table-lines"></span>
</button>
<span class="d-none d-md-block">{{ lang.syncjobs.sources_panel }} <span class="badge bg-info table-lines"></span></span>
<div class="btn-group ms-auto d-flex">
<button class="btn btn-xs btn-secondary refresh_table" data-draw="draw_imapsync_source_table" data-table="imapsync_source_table">{{ lang.admin.refresh }}</button>
</div>
</div>
<div id="collapse-tab-imapsync-sources" class="card-body collapse" data-bs-parent="#mail-content">
<p class="text-muted">{{ lang.syncjobs.sources_hint }}</p>
<div class="mass-actions-mailbox mb-4 d-none d-sm-block">
<div class="btn-group" data-acl="{{ acl.syncjobs }}">
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addImapsyncSourceModal"><i class="bi bi-plus-lg"></i> {{ lang.syncjobs.source_add }}</a>
</div>
</div>
<table id="imapsync_source_table" class="table table-striped dt-responsive w-100"></table>
<div class="mass-actions-mailbox mt-4">
<div class="btn-group" data-acl="{{ acl.syncjobs }}">
<a class="btn btn-sm btn-xs-lg btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addImapsyncSourceModal"><i class="bi bi-plus-lg"></i> {{ lang.syncjobs.source_add }}</a>
</div>
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex fs-5">
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-syncjobs" data-bs-toggle="collapse" aria-controls="collapse-tab-syncjobs">
{{ lang.syncjobs.sync_jobs }} <span class="badge bg-info table-lines"></span>
{{ lang.mailbox.sync_jobs }} <span class="badge bg-info table-lines"></span>
</button>
<span class="d-none d-md-block">{{ lang.syncjobs.sync_jobs }} <span class="badge bg-info table-lines"></span></span>
<span class="d-none d-md-block">{{ lang.mailbox.sync_jobs }} <span class="badge bg-info table-lines"></span></span>
<div class="btn-group ms-auto d-flex">
<button class="btn btn-xs btn-secondary refresh_table" data-draw="draw_sync_job_table" data-table="sync_job_table">{{ lang.admin.refresh }}</button>
@@ -51,7 +26,7 @@
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="sync_job_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="sync_job_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.syncjobs.create }}</a>
<a class="btn btn-sm btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.user.create_syncjob }}</a>
</div>
</div>
<table id="sync_job_table" class="table table-striped dt-responsive w-100"></table>
@@ -70,7 +45,7 @@
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-expand="sync_job_table">{{ lang.datatables.expand_all }}</a></li>
<li class="table_collapse_option"><a class="dropdown-item" data-datatables-collapse="sync_job_table">{{ lang.datatables.collapse_all }}</a></li>
</ul>
<a class="btn btn-sm btn-xs-lg btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.syncjobs.create }}</a>
<a class="btn btn-sm btn-xs-lg btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModalAdmin"><i class="bi bi-plus-lg"></i> {{ lang.user.create_syncjob }}</a>
</div>
</div>
</div>
@@ -1,161 +0,0 @@
<!-- add imapsync source modal (shared between user and admin views) -->
<div class="modal fade" id="addImapsyncSourceModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ lang.syncjobs.source_add }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_imapsync_source">
<div class="row mb-2">
<label class="control-label col-sm-2" for="name">{{ lang.syncjobs.source_name }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="name" maxlength="100" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="description">{{ lang.syncjobs.source_description }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="description" maxlength="255">
</div>
</div>
{% if mailcow_cc_role == 'admin' or mailcow_cc_role == 'domainadmin' %}
<div class="row mb-2">
<label class="control-label col-sm-2" for="scope">{{ lang.syncjobs.source_scope }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-scope" name="scope">
{% if mailcow_cc_role == 'admin' %}
<option value="all">{{ lang.syncjobs.source_scope_all }}</option>
{% endif %}
<option value="domain">{{ lang.syncjobs.source_scope_domain }}</option>
<option value="user" selected>{{ lang.syncjobs.source_scope_user }}</option>
</select>
</div>
</div>
<div class="row mb-2 imapsync-source-domains-row" style="display:none;">
<label class="control-label col-sm-2" for="domains">{{ lang.syncjobs.source_domains }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-domains" name="domains[]" data-live-search="true" title="{{ lang.syncjobs.source_domains }}" multiple></select>
</div>
</div>
<div class="row mb-2 imapsync-source-users-row" style="display:none;">
<label class="control-label col-sm-2" for="users">{{ lang.syncjobs.source_users }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-users" name="users[]" data-live-search="true" title="{{ lang.syncjobs.source_users }}" multiple></select>
</div>
</div>
{% endif %}
<div class="row mb-2">
<label class="control-label col-sm-2" for="host1">{{ lang.add.hostname }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="host1" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="port1">{{ lang.add.port }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="port1" min="1" max="65535" value="993" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="enc1">{{ lang.add.enc_method }}</label>
<div class="col-sm-10">
<select name="enc1" title="{{ lang.add.select }}" required>
<option value="SSL" selected>SSL</option>
<option value="TLS">STARTTLS</option>
<option value="PLAIN">PLAIN</option>
</select>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="auth_type">{{ lang.syncjobs.source_auth_type }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-auth-type" name="auth_type" required>
<option value="PLAIN" selected>PLAIN</option>
<option value="LOGIN">LOGIN</option>
<option value="CRAM-MD5">CRAM-MD5</option>
<option value="XOAUTH2">XOAUTH2 (OAuth2)</option>
</select>
</div>
</div>
<fieldset class="imapsync-source-oauth-block" style="display:none;">
<legend class="fs-5">{{ lang.syncjobs.source_oauth_block }}</legend>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_flow">{{ lang.syncjobs.source_oauth_flow }}</label>
<div class="col-sm-10">
<select class="form-control imapsync-source-oauth-flow" name="oauth_flow">
<option value="client_credentials" selected>{{ lang.syncjobs.source_oauth_flow_app }}</option>
<option value="authorization_code">{{ lang.syncjobs.source_oauth_flow_user }}</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_token_endpoint">Token-Endpoint</label>
<div class="col-sm-10">
<input type="url" class="form-control" name="oauth_token_endpoint" placeholder="https://provider.example.com/oauth2/token">
</div>
</div>
<div class="imapsync-source-authcode-block" style="display:none;">
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_authorize_endpoint">{{ lang.syncjobs.source_authorize_endpoint }}</label>
<div class="col-sm-10">
<input type="url" class="form-control" name="oauth_authorize_endpoint" placeholder="https://provider.example.com/oauth2/authorize">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_userinfo_endpoint">{{ lang.syncjobs.source_userinfo_endpoint }}</label>
<div class="col-sm-10">
<input type="url" class="form-control" name="oauth_userinfo_endpoint" placeholder="https://provider.example.com/oauth2/userinfo">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2">{{ lang.syncjobs.source_redirect_uri }}</label>
<div class="col-sm-10">
<input type="text" class="form-control imapsync-source-redirect-uri" readonly value="">
<small class="text-muted">{{ lang.syncjobs.source_redirect_uri_hint }}</small>
</div>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_client_id">Client-ID</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="oauth_client_id">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_client_secret">Client-Secret</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="oauth_client_secret">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="oauth_scope">Scope</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="oauth_scope" placeholder="openid email offline_access">
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="oauth_extra_params">Extra-Params (JSON)</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="oauth_extra_params" placeholder='{"resource":"https://provider.example.com"}'>
</div>
</div>
</fieldset>
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="active" checked> {{ lang.add.active }}</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="add_item" data-id="add_imapsync_source" data-api-url='add/syncjob_source' data-api-attr='{}' href="#">{{ lang.admin.add }}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
+39 -33
View File
@@ -937,11 +937,11 @@
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ lang.syncjobs.add }}</h3>
<h3 class="modal-title">{{ lang.add.syncjob }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted">{{ lang.syncjobs.add_hint }}</p>
<p class="text-muted">{{ lang.add.syncjob_hint }}</p>
<form class="form-horizontal" data-cached-form="false" role="form" data-id="add_syncjob">
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="username">{{ lang.add.username }}</label>
@@ -954,12 +954,16 @@
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="source_id">{{ lang.syncjobs.source_select }}</label>
<label class="control-label col-sm-2 text-sm-end" for="host1">{{ lang.add.hostname }}</label>
<div class="col-sm-10">
<select class="imapsync-source-select" data-live-search="true" name="source_id" title="{{ lang.syncjobs.source_select }}" required>
{# options are loaded via the API on modal open (populateImapsyncSourceSelects) #}
</select>
<small class="text-muted oauth-source-hint" style="display:none;">{{ lang.syncjobs.oauth_source_hint }}</small>
<input type="text" class="form-control" name="host1" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="port1">{{ lang.add.port }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="port1" min="1" max="65535" value="143" required>
<small class="text-muted">1-65535</small>
</div>
</div>
<div class="row mb-2">
@@ -968,118 +972,122 @@
<input type="text" class="form-control" name="user1" required>
</div>
</div>
<div class="row mb-2 imapsync-oauth-connect-row" style="display:none;">
<div class="offset-sm-2 col-sm-10">
<button type="button" class="btn btn-sm btn-primary imapsync-oauth-connect-btn">{{ lang.syncjobs.syncjob_oauth_connect }}</button>
<span class="imapsync-oauth-connect-status ms-2"></span>
</div>
</div>
<div class="row mb-2 password1-row">
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="password1">{{ lang.add.password }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="password1">
<input type="password" class="form-control" name="password1" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="mins_interval">{{ lang.syncjobs.mins_interval }}</label>
<label class="control-label col-sm-2 text-sm-end" for="enc1">{{ lang.add.enc_method }}</label>
<div class="col-sm-10">
<select name="enc1" title="{{ lang.add.select }}" required>
<option value="SSL" selected>SSL</option>
<option value="TLS">STARTTLS</option>
<option value="PLAIN">PLAIN</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="mins_interval">{{ lang.add.mins_interval }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="mins_interval" min="1" max="43800" value="20" required>
<small class="text-muted">1-43800</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="subfolder2">{{ lang.syncjobs.subfolder2|raw }}</label>
<label class="control-label col-sm-2 text-sm-end" for="subfolder2">{{ lang.edit.subfolder2|raw }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="subfolder2" value="">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="maxage">{{ lang.syncjobs.maxage|raw }}</label>
<label class="control-label col-sm-2 text-sm-end" for="maxage">{{ lang.edit.maxage|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxage" min="0" max="32000" value="0">
<small class="text-muted">0-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="maxbytespersecond">{{ lang.syncjobs.maxbytespersecond|raw }}</label>
<label class="control-label col-sm-2 text-sm-end" for="maxbytespersecond">{{ lang.edit.maxbytespersecond|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxbytespersecond" min="0" max="125000000" value="0">
<small class="text-muted">0-125000000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="timeout1">{{ lang.syncjobs.timeout1 }}</label>
<label class="control-label col-sm-2 text-sm-end" for="timeout1">{{ lang.edit.timeout1 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout1" min="1" max="32000" value="600">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="timeout2">{{ lang.syncjobs.timeout2 }}</label>
<label class="control-label col-sm-2 text-sm-end" for="timeout2">{{ lang.edit.timeout2 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout2" min="1" max="32000" value="600">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="exclude">{{ lang.syncjobs.exclude }}</label>
<label class="control-label col-sm-2 text-sm-end" for="exclude">{{ lang.add.exclude }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="exclude" value="(?i)spam|(?i)junk">
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2 text-sm-end" for="custom_params">{{ lang.syncjobs.custom_params }}</label>
<label class="control-label col-sm-2 text-sm-end" for="custom_params">{{ lang.add.custom_params }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="custom_params" placeholder="--some-param=xy --other-param=yx">
<small class="text-muted">{{ lang.syncjobs.custom_params_hint }}</small>
<small class="text-muted">{{ lang.add.custom_params_hint }}</small>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates" checked> {{ lang.syncjobs.delete2duplicates }} (--delete2duplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates" checked> {{ lang.add.delete2duplicates }} (--delete2duplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"> {{ lang.syncjobs.delete1 }} (--delete1)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"> {{ lang.add.delete1 }} (--delete1)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"> {{ lang.syncjobs.delete2 }} (--delete2)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"> {{ lang.add.delete2 }} (--delete2)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="automap" checked> {{ lang.syncjobs.automap }} (--automap)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="automap" checked> {{ lang.add.automap }} (--automap)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"> {{ lang.syncjobs.skipcrossduplicates }} (--skipcrossduplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"> {{ lang.add.skipcrossduplicates }} (--skipcrossduplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall" checked> {{ lang.syncjobs.subscribeall }} (--subscribeall)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall" checked> {{ lang.add.subscribeall }} (--subscribeall)</label>
</div>
</div>
</div>
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="dry"> {{ lang.syncjobs.dry }} (--dry)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="dry"> {{ lang.add.dry }} (--dry)</label>
</div>
</div>
</div>
@@ -1339,5 +1347,3 @@
</div>
</div>
</div><!-- DNS info modal -->
{% include "modals/imapsync_source.twig" %}
+39 -32
View File
@@ -31,19 +31,23 @@
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ lang.syncjobs.add }}</h3>
<h3 class="modal-title">{{ lang.add.syncjob }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>{{ lang.syncjobs.add_hint }}</p>
<p>{{ lang.add.syncjob_hint }}</p>
<form class="form-horizontal" data-cached-form="true" role="form" data-id="add_syncjob">
<div class="row mb-4">
<label class="control-label col-sm-2" for="source_id">{{ lang.syncjobs.source_select }}</label>
<div class="row mb-2">
<label class="control-label col-sm-2" for="host1">{{ lang.add.hostname }}</label>
<div class="col-sm-10">
<select class="imapsync-source-select" data-live-search="true" name="source_id" title="{{ lang.syncjobs.source_select }}" required>
{# options are loaded via the API on modal open (populateImapsyncSourceSelects) #}
</select>
<small class="text-muted oauth-source-hint" style="display:none;">{{ lang.syncjobs.oauth_source_hint }}</small>
<input type="text" class="form-control" name="host1" required>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="port1">{{ lang.add.port }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="port1" min="1" max="65535" value="143" required>
<small class="text-muted">1-65535</small>
</div>
</div>
<div class="row mb-2">
@@ -52,67 +56,71 @@
<input type="text" class="form-control" name="user1" required>
</div>
</div>
<div class="row mb-2 imapsync-oauth-connect-row" style="display:none;">
<div class="offset-sm-2 col-sm-10">
<button type="button" class="btn btn-sm btn-primary imapsync-oauth-connect-btn">{{ lang.syncjobs.syncjob_oauth_connect }}</button>
<span class="imapsync-oauth-connect-status ms-2"></span>
</div>
</div>
<div class="row mb-4 password1-row">
<div class="row mb-4">
<label class="control-label col-sm-2" for="password1">{{ lang.add.password }}</label>
<div class="col-sm-10">
<input type="password" class="form-control" name="password1" data-hibp="true">
<input type="password" class="form-control" name="password1" data-hibp="true" required>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="mins_interval">{{ lang.syncjobs.mins_interval }}</label>
<label class="control-label col-sm-2" for="enc1">{{ lang.add.enc_method }}</label>
<div class="col-sm-10">
<select name="enc1" title="{{ lang.add.select }}" required>
<option value="SSL" selected>SSL</option>
<option value="TLS">STARTTLS</option>
<option value="PLAIN">PLAIN</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="mins_interval">{{ lang.add.mins_interval }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="mins_interval" min="1" max="43800" value="20" required>
<small class="text-muted">1-43800</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="subfolder2">{{ lang.syncjobs.subfolder2|raw }}</label>
<label class="control-label col-sm-2" for="subfolder2">{{ lang.edit.subfolder2|raw }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="subfolder2" value="">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="maxage">{{ lang.syncjobs.maxage|raw }}</label>
<label class="control-label col-sm-2" for="maxage">{{ lang.edit.maxage|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxage" min="0" max="32000" value="0">
<small class="text-muted">0-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="maxbytespersecond">{{ lang.syncjobs.maxbytespersecond|raw }}</label>
<label class="control-label col-sm-2" for="maxbytespersecond">{{ lang.edit.maxbytespersecond|raw }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="maxbytespersecond" min="0" max="125000000" value="0">
<small class="text-muted">0-125000000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="timeout1">{{ lang.syncjobs.timeout1 }}</label>
<label class="control-label col-sm-2" for="timeout1">{{ lang.edit.timeout1 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout1" min="1" max="32000" value="600">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="timeout2">{{ lang.syncjobs.timeout2 }}</label>
<label class="control-label col-sm-2" for="timeout2">{{ lang.edit.timeout2 }}</label>
<div class="col-sm-10">
<input type="number" class="form-control" name="timeout2" min="1" max="32000" value="600">
<small class="text-muted">1-32000</small>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="exclude">{{ lang.syncjobs.exclude }}</label>
<label class="control-label col-sm-2" for="exclude">{{ lang.add.exclude }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="exclude" value="(?i)spam|(?i)junk">
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="custom_params">{{ lang.syncjobs.custom_params }}</label>
<label class="control-label col-sm-2" for="custom_params">{{ lang.add.custom_params }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="custom_params" placeholder="--delete2folders --otheroption">
</div>
@@ -120,49 +128,49 @@
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates" checked> {{ lang.syncjobs.delete2duplicates }} (--delete2duplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2duplicates" checked> {{ lang.add.delete2duplicates }} (--delete2duplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"> {{ lang.syncjobs.delete1 }} (--delete1)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete1"> {{ lang.add.delete1 }} (--delete1)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"> {{ lang.syncjobs.delete2 }} (--delete2)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="delete2"> {{ lang.add.delete2 }} (--delete2)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="automap" checked> {{ lang.syncjobs.automap }} (--automap)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="automap" checked> {{ lang.add.automap }} (--automap)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"> {{ lang.syncjobs.skipcrossduplicates }} (--skipcrossduplicates)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="skipcrossduplicates"> {{ lang.add.skipcrossduplicates }} (--skipcrossduplicates)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall" checked> {{ lang.syncjobs.subscribeall }} (--subscribeall)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="subscribeall" checked> {{ lang.add.subscribeall }} (--subscribeall)</label>
</div>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="dry" checked> {{ lang.syncjobs.dry }} (--dry)</label>
<label><input type="checkbox" class="form-check-input" value="1" name="dry" checked> {{ lang.add.dry }} (--dry)</label>
</div>
</div>
</div>
@@ -183,7 +191,6 @@
</div>
</div>
</div><!-- add sync job modal -->
{% include 'modals/imapsync_source.twig' %}
<!-- app passwd modal -->
<div class="modal fade" id="addAppPasswdModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg">
+1 -1
View File
@@ -19,7 +19,7 @@
<li class="nav-item" role="presentation"><button class="nav-link" role="tab" aria-selected="false" aria-controls="Spamfilter" role="tab" data-bs-toggle="tab" data-bs-target="#Spamfilter">{{ lang.user.spamfilter }}</button></li>
{% endif %}
{% if acl.syncjobs == 1 %}
<li class="nav-item" role="presentation"><button class="nav-link" role="tab" aria-selected="false" aria-controls="Syncjobs" role="tab" data-bs-toggle="tab" data-bs-target="#Syncjobs">{{ lang.syncjobs.sync_jobs }}</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" role="tab" aria-selected="false" aria-controls="Syncjobs" role="tab" data-bs-toggle="tab" data-bs-target="#Syncjobs">{{ lang.user.sync_jobs }}</button></li>
{% endif %}
{% if acl.app_passwds == 1 %}
<li class="nav-item" role="presentation"><button class="nav-link" role="tab" aria-selected="false" aria-controls="AppPasswds" role="tab" data-bs-toggle="tab" data-bs-target="#AppPasswds">{{ lang.user.app_passwds }}</button></li>
+5 -22
View File
@@ -1,29 +1,12 @@
<div class="tab-pane fade" id="Syncjobs" role="tabpanel" aria-labelledby="Syncjobs">
<div class="card mb-4">
<div class="card-header d-flex fs-5">
<span class="d-none d-md-block">{{ lang.syncjobs.sources_panel }}</span>
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-imapsync-sources" data-bs-toggle="collapse" aria-controls="collapse-tab-imapsync-sources">
{{ lang.syncjobs.sources_panel }}
</button>
</div>
<div id="collapse-tab-imapsync-sources" class="card-body collapse" data-bs-parent="#user-content">
<p class="text-muted">{{ lang.syncjobs.sources_hint }}</p>
<div class="mass-actions-user mb-4">
<div class="btn-group" data-acl="{{ acl.syncjobs }}">
<a class="btn btn-sm d-block d-sm-inline btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addImapsyncSourceModal"><i class="bi bi-plus-lg"></i> {{ lang.syncjobs.source_add }}</a>
</div>
</div>
<table id="imapsync_source_table" class="table table-striped dt-responsive w-100"></table>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex fs-5">
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-Syncjobs" data-bs-toggle="collapse" aria-controls="collapse-tab-Syncjobs">
{{ lang.syncjobs.sync_jobs }}
{{ lang.user.sync_jobs }}
</button>
<span class="d-none d-md-block">{{ lang.syncjobs.sync_jobs }}</span>
<span class="d-none d-md-block">{{ lang.user.sync_jobs }}</span>
</div>
<div id="collapse-tab-Syncjobs" class="card-body collapse" data-bs-parent="#user-content">
<div id="collapse-tab-Syncjobs" class="card-body collapse" data-bs-parent="#user-content">
<div class="mass-actions-user mb-4">
<div class="btn-group" data-acl="{{ acl.syncjobs }}">
<div class="btn-group">
@@ -40,7 +23,7 @@
</ul>
</div>
<div class="btn-group">
<a class="btn btn-sm d-block d-sm-inline btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModal"><i class="bi bi-plus-lg"></i> {{ lang.syncjobs.create }}</a>
<a class="btn btn-sm d-block d-sm-inline btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModal"><i class="bi bi-plus-lg"></i> {{ lang.user.create_syncjob }}</a>
</div>
</div>
</div>
@@ -61,7 +44,7 @@
</ul>
</div>
<div class="btn-group">
<a class="btn btn-sm d-block d-sm-inline btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModal"><i class="bi bi-plus-lg"></i> {{ lang.syncjobs.create }}</a>
<a class="btn btn-sm d-block d-sm-inline btn-success" href="#" data-bs-toggle="modal" data-bs-target="#addSyncJobModal"><i class="bi bi-plus-lg"></i> {{ lang.user.create_syncjob }}</a>
</div>
</div>
</div>
@@ -3,7 +3,6 @@
<script type='text/javascript'>
var acl = '{{ acl_json|raw }}';
var lang = {{ lang_user|raw }};
lang.syncjobs = {{ lang_syncjobs|raw }};
var csrf_token = '{{ csrf_token }}';
var pagination_size = Math.trunc('{{ pagination_size }}');
var mailcow_cc_username = '{{ mailcow_cc_username }}';
+5 -8
View File
@@ -84,7 +84,7 @@ services:
- clamd
rspamd-mailcow:
image: ghcr.io/mailcow/rspamd:3.14.3-1
image: ghcr.io/mailcow/rspamd:4.1.0-1
stop_grace_period: 30s
depends_on:
- dovecot-mailcow
@@ -117,7 +117,7 @@ services:
- rspamd
php-fpm-mailcow:
image: ghcr.io/mailcow/phpfpm:8.2.29-2
image: ghcr.io/mailcow/phpfpm:8.2.29-3
command: "php-fpm -d date.timezone=${TZ} -d expose_php=0"
depends_on:
- redis-mailcow
@@ -194,16 +194,13 @@ services:
ofelia.job-exec.phpfpm_ldap_sync.schedule: "0 * * * * *"
ofelia.job-exec.phpfpm_ldap_sync.no-overlap: "true"
ofelia.job-exec.phpfpm_ldap_sync.command: "/bin/bash -c \"php /crons/ldap-sync.php || exit 0\""
ofelia.job-exec.phpfpm_imapsync_oauth_refresh.schedule: "0 * * * * *"
ofelia.job-exec.phpfpm_imapsync_oauth_refresh.no-overlap: "true"
ofelia.job-exec.phpfpm_imapsync_oauth_refresh.command: "/bin/bash -c \"php /crons/imapsync-oauth-refresh.php || exit 0\""
networks:
mailcow-network:
aliases:
- phpfpm
sogo-mailcow:
image: ghcr.io/mailcow/sogo:5.12.8-1
image: ghcr.io/mailcow/sogo:5.12.9-1
environment:
- DBNAME=${DBNAME}
- DBUSER=${DBUSER}
@@ -255,7 +252,7 @@ services:
- sogo
dovecot-mailcow:
image: ghcr.io/mailcow/dovecot:2.3.21.1-3
image: ghcr.io/mailcow/dovecot:2.3.21.1-2
depends_on:
- mysql-mailcow
- netfilter-mailcow
@@ -422,7 +419,7 @@ services:
- php-fpm-mailcow
- sogo-mailcow
- rspamd-mailcow
image: ghcr.io/mailcow/nginx:1.30.2-1
image: ghcr.io/mailcow/nginx:1.30.3-1
dns:
- ${IPV4_NETWORK:-172.22.1}.254
environment:
@@ -25,6 +25,6 @@ services:
- /var/run/mysqld/mysqld.sock:/var/run/mysqld/mysqld.sock
mysql-mailcow:
image: alpine:3.23
image: alpine:3.24
command: /bin/true
restart: "no"