first commit
This commit is contained in:
commit
19dfc51360
85 files changed
+25504
No files matched your search
@@ -0,0 +1,73 @@
|
||||
// Regenerate docs/images/*.png from a running ActivePush panel (Playwright).
|
||||
// node tools/dev/screenshots.js http://127.0.0.1:9091 admin 'password' [outdir]
|
||||
// With SETUP_TOKEN=<token> in the environment the first-run screen is captured and the
|
||||
// administrator is created with the given credentials.
|
||||
const { chromium } = require('playwright');
|
||||
const path = require('path');
|
||||
|
||||
(async () => {
|
||||
const [base, user, pass, out = path.join(__dirname, '../../docs/images')] = process.argv.slice(2);
|
||||
if (!base || !user || !pass) {
|
||||
console.error('usage: node screenshots.js URL USER PASSWORD [OUTDIR]');
|
||||
process.exit(2);
|
||||
}
|
||||
const browser = await chromium.launch();
|
||||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, locale: 'en-US', deviceScaleFactor: 1 });
|
||||
const page = await ctx.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
const shot = async (name, full = false) => {
|
||||
await page.screenshot({ path: path.join(out, `${name}.png`), fullPage: full });
|
||||
console.log('saved', name);
|
||||
};
|
||||
|
||||
await page.goto(base + '/');
|
||||
await page.waitForSelector('#auth:not([hidden])');
|
||||
if (process.env.SETUP_TOKEN) {
|
||||
await page.fill('#setup-token', process.env.SETUP_TOKEN);
|
||||
await page.fill('#username', user);
|
||||
await shot('setup');
|
||||
await page.fill('#password', pass);
|
||||
await page.fill('#password2', pass);
|
||||
} else {
|
||||
await page.fill('#username', user);
|
||||
await page.fill('#password', pass);
|
||||
}
|
||||
await page.click('#auth-submit');
|
||||
await page.waitForSelector('#shell:not([hidden])');
|
||||
|
||||
const pages = [
|
||||
['overview', true, 5000], ['live', false, 6000], ['devices', false, 2500], ['users', false, 2500],
|
||||
['backends', false, 1500], ['activesync', false, 1500], ['policies', false, 1500], ['security', false, 1500],
|
||||
['logs', false, 4000],
|
||||
];
|
||||
for (const [id, full, wait] of pages) {
|
||||
await page.goto(`${base}/#${id}`);
|
||||
await page.waitForTimeout(wait);
|
||||
await shot(id === 'users' ? 'mailboxes' : id, full);
|
||||
}
|
||||
await page.goto(`${base}/#devices`);
|
||||
await page.waitForTimeout(2000);
|
||||
const chips = await page.$$('.chip[data-pid]');
|
||||
if (chips.length) {
|
||||
await chips[0].click();
|
||||
await page.waitForTimeout(1200);
|
||||
await shot('device');
|
||||
}
|
||||
await page.goto(`${base}/#overview`);
|
||||
await page.waitForTimeout(500);
|
||||
await page.click('#theme');
|
||||
await page.waitForTimeout(4000);
|
||||
await shot('overview-light');
|
||||
await page.click('#theme');
|
||||
|
||||
const mobile = await browser.newContext({ viewport: { width: 390, height: 844 }, locale: 'en-US', deviceScaleFactor: 2, storageState: await ctx.storageState() });
|
||||
const mp = await mobile.newPage();
|
||||
await mp.goto(`${base}/#overview`);
|
||||
await mp.waitForTimeout(4000);
|
||||
await mp.screenshot({ path: path.join(out, 'mobile.png') });
|
||||
console.log('saved mobile');
|
||||
|
||||
if (errors.length) console.error('page errors:', errors);
|
||||
await browser.close();
|
||||
})();
|
||||
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local throw-away backends for development and end-to-end tests:
|
||||
# Dovecot IMAP 127.0.0.1:11143 (plain, test only)
|
||||
# Radicale 127.0.0.1:15232 (CalDAV + CardDAV)
|
||||
# SMTP sink 127.0.0.1:12525 (prints/stores every message, no auth)
|
||||
# Test account: jan@test.local / Test-Password-123
|
||||
#
|
||||
# NEVER use this configuration in production. Ctrl+C stops everything.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
DIR="${AP_TEST_DIR:-$ROOT/data/test-backends}"
|
||||
RUN_USER="${AP_TEST_USER:-$(id -un)}"
|
||||
if [[ "$RUN_USER" == "root" ]]; then
|
||||
printf 'Run as a normal user or set AP_TEST_USER=<unprivileged user>.\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
for bin in dovecot radicale python3; do
|
||||
command -v "$bin" >/dev/null 2>&1 || { printf 'Missing %s (apt install dovecot-imapd; pip install radicale aiosmtpd)\n' "$bin" >&2; exit 127; }
|
||||
done
|
||||
python3 -c 'import aiosmtpd' 2>/dev/null || { printf 'Missing python aiosmtpd (pip install aiosmtpd)\n' >&2; exit 127; }
|
||||
UIDN="$(id -u "$RUN_USER")"; GIDN="$(id -g "$RUN_USER")"
|
||||
mkdir -p "$DIR"/{mail,run,radicale,sink}
|
||||
chown -R "$RUN_USER" "$DIR" 2>/dev/null || true
|
||||
|
||||
cat > "$DIR/users" <<EOF
|
||||
jan@test.local:{PLAIN}Test-Password-123:$UIDN:$GIDN::$DIR/mail/jan
|
||||
anna@test.local:{PLAIN}Test-Password-123:$UIDN:$GIDN::$DIR/mail/anna
|
||||
biuro@test.local:{PLAIN}Test-Password-123:$UIDN:$GIDN::$DIR/mail/biuro
|
||||
kasia@test.local:{PLAIN}Test-Password-123:$UIDN:$GIDN::$DIR/mail/kasia
|
||||
EOF
|
||||
cat > "$DIR/dovecot.conf" <<EOF
|
||||
protocols = imap
|
||||
listen = 127.0.0.1
|
||||
base_dir = $DIR/run
|
||||
state_dir = $DIR/run
|
||||
default_internal_user = $RUN_USER
|
||||
default_login_user = $RUN_USER
|
||||
default_internal_group = $(id -gn "$RUN_USER")
|
||||
first_valid_uid = 1
|
||||
ssl = no
|
||||
disable_plaintext_auth = no
|
||||
auth_mechanisms = plain login
|
||||
log_path = $DIR/dovecot.log
|
||||
mail_location = maildir:~/Maildir
|
||||
passdb {
|
||||
driver = passwd-file
|
||||
args = $DIR/users
|
||||
}
|
||||
userdb {
|
||||
driver = passwd-file
|
||||
args = $DIR/users
|
||||
}
|
||||
service imap-login {
|
||||
inet_listener imap {
|
||||
address = 127.0.0.1
|
||||
port = 11143
|
||||
}
|
||||
inet_listener imaps {
|
||||
port = 0
|
||||
}
|
||||
}
|
||||
service anvil {
|
||||
chroot =
|
||||
}
|
||||
service imap-login {
|
||||
chroot =
|
||||
}
|
||||
namespace inbox {
|
||||
inbox = yes
|
||||
separator = /
|
||||
mailbox Drafts {
|
||||
special_use = \Drafts
|
||||
auto = subscribe
|
||||
}
|
||||
mailbox Sent {
|
||||
special_use = \Sent
|
||||
auto = subscribe
|
||||
}
|
||||
mailbox Trash {
|
||||
special_use = \Trash
|
||||
auto = subscribe
|
||||
}
|
||||
mailbox Junk {
|
||||
special_use = \Junk
|
||||
auto = subscribe
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Radicale: htpasswd plain
|
||||
for u in jan anna biuro kasia; do printf '%s@test.local:Test-Password-123\n' "$u"; done > "$DIR/radicale/users"
|
||||
cat > "$DIR/radicale/config" <<EOF
|
||||
[server]
|
||||
hosts = 127.0.0.1:15232
|
||||
[auth]
|
||||
type = htpasswd
|
||||
htpasswd_filename = $DIR/radicale/users
|
||||
htpasswd_encryption = plain
|
||||
[storage]
|
||||
filesystem_folder = $DIR/radicale/collections
|
||||
[rights]
|
||||
type = owner_only
|
||||
EOF
|
||||
|
||||
cat > "$DIR/sink.py" <<'EOF'
|
||||
import asyncio, sys, time, os
|
||||
from aiosmtpd.controller import Controller
|
||||
out = sys.argv[1]
|
||||
class H:
|
||||
async def handle_DATA(self, server, session, envelope):
|
||||
name = os.path.join(out, f"{time.time():.6f}.eml")
|
||||
with open(name, "wb") as f:
|
||||
f.write(f"X-Envelope-From: {envelope.mail_from}\r\nX-Envelope-To: {', '.join(envelope.rcpt_tos)}\r\n".encode())
|
||||
f.write(envelope.content)
|
||||
print(f"[smtp-sink] {envelope.mail_from} -> {envelope.rcpt_tos} ({len(envelope.content)} bytes) saved {name}", flush=True)
|
||||
return "250 OK"
|
||||
c = Controller(H(), hostname="127.0.0.1", port=12525)
|
||||
c.start()
|
||||
print("[smtp-sink] listening on 127.0.0.1:12525", flush=True)
|
||||
try:
|
||||
asyncio.new_event_loop().run_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
EOF
|
||||
|
||||
pids=()
|
||||
cleanup() { for p in "${pids[@]}"; do kill "$p" 2>/dev/null || true; done; dovecot -c "$DIR/dovecot.conf" stop 2>/dev/null || true; }
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
dovecot -c "$DIR/dovecot.conf"
|
||||
if [[ "$(id -un)" == "$RUN_USER" ]]; then
|
||||
radicale -C "$DIR/radicale/config" >"$DIR/radicale.log" 2>&1 & pids+=($!)
|
||||
python3 "$DIR/sink.py" "$DIR/sink" & pids+=($!)
|
||||
else
|
||||
su -s /bin/bash "$RUN_USER" -c "radicale -C '$DIR/radicale/config' >'$DIR/radicale.log' 2>&1" & pids+=($!)
|
||||
su -s /bin/bash "$RUN_USER" -c "python3 '$DIR/sink.py' '$DIR/sink'" & pids+=($!)
|
||||
fi
|
||||
sleep 1
|
||||
cat <<EOF
|
||||
Test backends running (data in $DIR):
|
||||
IMAP 127.0.0.1:11143 (security: none)
|
||||
CalDAV http://127.0.0.1:15232 path /%u/
|
||||
CardDAV http://127.0.0.1:15232 path /%u/
|
||||
SMTP 127.0.0.1:12525 (auth: none, security: none) -> $DIR/sink
|
||||
accounts jan, anna, biuro, kasia @test.local, password Test-Password-123
|
||||
Apply matching settings with: ./dev.sh config apply tools/dev/test-settings.json
|
||||
Press Ctrl+C to stop.
|
||||
EOF
|
||||
wait
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"general": { "server_name": "ActivePush DEV", "default_timezone": "Europe/Warsaw", "login_format": "as_given", "default_domain": "test.local", "email_template": "{login}" },
|
||||
"imap": { "host": "127.0.0.1", "port": 11143, "security": "none", "verify_tls": false },
|
||||
"smtp": { "method": "smtp", "host": "127.0.0.1", "port": 12525, "security": "none", "auth": "none", "verify_tls": false },
|
||||
"caldav": { "enabled": true, "base_url": "http://127.0.0.1:15232", "path": "/%u/", "verify_tls": false },
|
||||
"carddav": { "enabled": true, "base_url": "http://127.0.0.1:15232", "path": "/%u/", "verify_tls": false, "gal_min_length": 2 },
|
||||
"ping": { "check_interval_secs": 5, "lifetime_min": 30, "lifetime_max": 3540, "max_folders": 300 },
|
||||
"sync": { "heartbeat_min": 30 },
|
||||
"security": { "auth_fail_max": 20 }
|
||||
}
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convert an existing Z-Push installation's configuration into ActivePush settings JSON.
|
||||
|
||||
Usage:
|
||||
python3 zpush-import.py /usr/share/z-push # source layout (config.php, backend/*/config.php)
|
||||
python3 zpush-import.py /etc/z-push # Debian package layout (*.conf.php)
|
||||
python3 zpush-import.py /var/www/z-push > zpush.json
|
||||
active-push config apply zpush.json
|
||||
|
||||
Only settings are converted. Device sync states are not portable: devices notice the new
|
||||
server (unknown sync key / policy key) and resynchronise automatically.
|
||||
"""
|
||||
import configparser
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
DEFINE = re.compile(r"""^\s*define\s*\(\s*['"]([A-Z0-9_]+)['"]\s*,\s*(.+?)\)\s*;""", re.S)
|
||||
|
||||
|
||||
def php_value(raw):
|
||||
raw = raw.strip()
|
||||
if raw.startswith(("'", '"')) and raw.endswith(raw[0]):
|
||||
return raw[1:-1].replace("\\'", "'").replace('\\"', '"')
|
||||
low = raw.lower()
|
||||
if low in ("true", "false"):
|
||||
return low == "true"
|
||||
if re.fullmatch(r"-?\d+", raw):
|
||||
return int(raw)
|
||||
if low.startswith("array(") or raw.startswith("["):
|
||||
inner = raw[raw.index("(") + 1: raw.rindex(")")] if low.startswith("array(") else raw[1:-1]
|
||||
out = {}
|
||||
for k, v in re.findall(r"""['"]([^'"]+)['"]\s*=>\s*('[^']*'|"[^"]*"|[^,\)]+)""", inner):
|
||||
out[k] = php_value(v)
|
||||
return out
|
||||
if low.startswith("serialize("):
|
||||
return None
|
||||
return {"__const__": raw}
|
||||
|
||||
|
||||
def read_defines(paths):
|
||||
defs = {}
|
||||
for p in paths:
|
||||
text = p.read_text(errors="replace")
|
||||
# strip block comments and line comments
|
||||
text = re.sub(r"/\*.*?\*/", "", text, flags=re.S)
|
||||
lines = [l for l in text.splitlines() if not l.strip().startswith(("//", "#"))]
|
||||
joined = "\n".join(lines)
|
||||
for m in re.finditer(r"define\s*\(.*?\)\s*;", joined, re.S):
|
||||
d = DEFINE.match(m.group(0))
|
||||
if d:
|
||||
defs[d.group(1)] = php_value(d.group(2))
|
||||
return defs
|
||||
|
||||
|
||||
def find_configs(root):
|
||||
root = pathlib.Path(root)
|
||||
files = []
|
||||
for pat in ["config.php", "*.conf.php", "backend/*/config.php", "autodiscover/config.php", "src/config.php", "src/backend/*/config.php", "src/autodiscover/config.php"]:
|
||||
files += [f for f in root.glob(pat) if f.is_file()]
|
||||
policies = [f for f in [root / "policies.ini", root / "src/policies.ini"] if f.is_file()]
|
||||
return sorted(set(files)), policies
|
||||
|
||||
|
||||
FILTER = {"SYNC_FILTERTYPE_ALL": 0, "SYNC_FILTERTYPE_1DAY": 1, "SYNC_FILTERTYPE_3DAYS": 2, "SYNC_FILTERTYPE_1WEEK": 3,
|
||||
"SYNC_FILTERTYPE_2WEEKS": 4, "SYNC_FILTERTYPE_1MONTH": 5, "SYNC_FILTERTYPE_3MONTHS": 6, "SYNC_FILTERTYPE_6MONTHS": 7}
|
||||
|
||||
POLICY_KEYS = {
|
||||
"devpwenabled": bool, "alphanumpwreq": bool, "devencenabled": bool, "attenabled": bool, "mindevpwlenngth": int,
|
||||
"maxinacttimedevlock": int, "maxdevpwfailedattempts": int, "maxattsize": int, "allowsimpledevpw": bool,
|
||||
"devpwexpiration": int, "devpwhistory": int, "allowstoragecard": bool, "allowcam": bool, "reqdevenc": bool,
|
||||
"allowunsignedapps": bool, "mindevcomplexchars": int, "allowwifi": bool, "allowtextmessaging": bool,
|
||||
"allowpopimapemail": bool, "allowbluetooth": int, "allowirda": bool, "reqmansyncroam": bool, "maxcalagefilter": int,
|
||||
"allowhtmlemail": bool, "maxemailagefilter": int, "maxemailbodytruncsize": int, "maxemailhtmlbodytruncsize": int,
|
||||
"reqsignedsmimemessages": bool, "reqencsmimemessages": bool, "reqsignedsmimealgorithm": int,
|
||||
"reqencsmimealgorithm": int, "allowsmimeencalgneg": int, "allowsmimesoftcerts": bool, "allowbrowser": bool,
|
||||
"allowconsumeremail": bool, "allowinternetsharing": bool, "allowremotedesktop": bool, "allowdesktopsync": bool,
|
||||
"allowunsignedinstallationpackages": bool, "pwrecoveryenabled": bool,
|
||||
}
|
||||
|
||||
|
||||
def parse_policies(path):
|
||||
cp = configparser.ConfigParser(interpolation=None, strict=False)
|
||||
cp.read_string(path.read_text(errors="replace"))
|
||||
out = []
|
||||
for sec in cp.sections():
|
||||
p = {"name": sec}
|
||||
for k, typ in POLICY_KEYS.items():
|
||||
if k in cp[sec]:
|
||||
v = cp[sec][k].strip().strip("'\"")
|
||||
if v == "":
|
||||
continue
|
||||
try:
|
||||
p[k] = (int(v) != 0) if typ is bool else int(v)
|
||||
except ValueError:
|
||||
pass
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 2:
|
||||
print(__doc__, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
files, pol_files = find_configs(sys.argv[1])
|
||||
if not files:
|
||||
sys.exit(f"no Z-Push configuration files found in {sys.argv[1]}")
|
||||
d = read_defines(files)
|
||||
notes = [f"read {f}" for f in files]
|
||||
s = {"general": {}, "imap": {}, "smtp": {}, "caldav": {}, "carddav": {}, "sync": {}, "ping": {}, "provisioning": {}, "autodiscover": {}}
|
||||
|
||||
if isinstance(d.get("TIMEZONE"), str) and d["TIMEZONE"]:
|
||||
s["general"]["default_timezone"] = d["TIMEZONE"]
|
||||
if "USE_FULLEMAIL_FOR_LOGIN" in d:
|
||||
s["general"]["login_format"] = "as_given" if d["USE_FULLEMAIL_FOR_LOGIN"] else "local_part"
|
||||
|
||||
# IMAP
|
||||
if "IMAP_SERVER" in d:
|
||||
s["imap"]["host"] = d["IMAP_SERVER"]
|
||||
if "IMAP_PORT" in d:
|
||||
s["imap"]["port"] = int(d["IMAP_PORT"])
|
||||
opts = str(d.get("IMAP_OPTIONS", "")).lower()
|
||||
if opts:
|
||||
if "/ssl" in opts:
|
||||
s["imap"]["security"] = "tls"
|
||||
elif "/tls" in opts:
|
||||
s["imap"]["security"] = "start_tls"
|
||||
elif "/notls" in opts:
|
||||
s["imap"]["security"] = "none"
|
||||
s["imap"]["verify_tls"] = "novalidate-cert" not in opts
|
||||
for zk, ak in [("IMAP_FOLDER_PREFIX", "folder_prefix"), ("IMAP_FOLDER_INBOX", "folder_inbox"), ("IMAP_FOLDER_SENT", "folder_sent"),
|
||||
("IMAP_FOLDER_DRAFT", "folder_drafts"), ("IMAP_FOLDER_TRASH", "folder_trash"), ("IMAP_FOLDER_SPAM", "folder_spam"),
|
||||
("IMAP_FOLDER_ARCHIVE", "folder_archive")]:
|
||||
if isinstance(d.get(zk), str):
|
||||
s["imap"][ak] = d[zk]
|
||||
if "IMAP_AUTOSEEN_ON_DELETE" in d:
|
||||
s["imap"]["autoseen_on_delete"] = bool(d["IMAP_AUTOSEEN_ON_DELETE"])
|
||||
if "IMAP_INLINE_FORWARD" in d:
|
||||
s["imap"]["inline_forward"] = bool(d["IMAP_INLINE_FORWARD"])
|
||||
if isinstance(d.get("IMAP_EXCLUDED_FOLDERS"), str) and d["IMAP_EXCLUDED_FOLDERS"]:
|
||||
s["imap"]["excluded_folders"] = [x for x in d["IMAP_EXCLUDED_FOLDERS"].split("|") if x]
|
||||
df = d.get("IMAP_DEFAULTFROM")
|
||||
if isinstance(df, str):
|
||||
if df.startswith("@"):
|
||||
s["general"]["email_template"] = "{local}" + df
|
||||
elif df == "domain":
|
||||
s["general"]["email_template"] = "{local}@{domain}"
|
||||
elif df in ("", "username"):
|
||||
s["general"]["email_template"] = "{login}"
|
||||
elif df in ("sql", "ldap"):
|
||||
notes.append("IMAP_DEFAULTFROM sql/ldap is not supported; using {login} - adjust email_template")
|
||||
|
||||
# SMTP
|
||||
method = str(d.get("IMAP_SMTP_METHOD", "")).lower()
|
||||
if method in ("mail", "sendmail"):
|
||||
s["smtp"]["method"] = "sendmail"
|
||||
notes.append("SMTP method 'mail'/'sendmail' -> sendmail (consider SMTP submission on port 587 instead)")
|
||||
elif method == "smtp":
|
||||
s["smtp"]["method"] = "smtp"
|
||||
params = d.get("IMAP_SMTP_PARAMS") if isinstance(d.get("IMAP_SMTP_PARAMS"), dict) else {}
|
||||
host = str(params.get("host", "localhost"))
|
||||
if host.startswith("ssl://") or host.startswith("tls://"):
|
||||
s["smtp"]["security"] = "tls"
|
||||
host = host.split("://", 1)[1]
|
||||
s["smtp"]["host"] = host
|
||||
s["smtp"]["port"] = int(params.get("port", 25))
|
||||
if s["smtp"]["port"] == 587:
|
||||
s["smtp"]["security"] = "start_tls"
|
||||
if params.get("auth"):
|
||||
if params.get("username") == "imap_username":
|
||||
s["smtp"]["auth"] = "user_credentials"
|
||||
else:
|
||||
s["smtp"]["auth"] = "static"
|
||||
s["smtp"]["username"] = str(params.get("username", ""))
|
||||
s["smtp"]["password"] = str(params.get("password", ""))
|
||||
else:
|
||||
s["smtp"]["auth"] = "none"
|
||||
if params.get("verify_peer") is False:
|
||||
s["smtp"]["verify_tls"] = False
|
||||
|
||||
# CalDAV / CardDAV
|
||||
def dav(prefix, key):
|
||||
server = d.get(f"{prefix}_SERVER")
|
||||
if not isinstance(server, str) or server in ("", "caldavserver.domain.com"):
|
||||
return
|
||||
proto = d.get(f"{prefix}_PROTOCOL", "https")
|
||||
port = d.get(f"{prefix}_PORT", 443 if proto == "https" else 80)
|
||||
default_port = (proto == "https" and str(port) == "443") or (proto == "http" and str(port) == "80")
|
||||
s[key]["enabled"] = True
|
||||
s[key]["base_url"] = f"{proto}://{server}" + ("" if default_port else f":{port}")
|
||||
if isinstance(d.get(f"{prefix}_PATH"), str):
|
||||
s[key]["path"] = d[f"{prefix}_PATH"]
|
||||
if f"{prefix}_PATH_INCLUDE" in d and isinstance(d[f"{prefix}_PATH_INCLUDE"], str):
|
||||
s[key]["include_regex"] = d[f"{prefix}_PATH_INCLUDE"].strip("/")
|
||||
if f"{prefix}_PATH_EXCLUDE" in d and isinstance(d[f"{prefix}_PATH_EXCLUDE"], str):
|
||||
s[key]["exclude_regex"] = d[f"{prefix}_PATH_EXCLUDE"].strip("/")
|
||||
ssl = d.get("DAV_SSL_VERIFY")
|
||||
if isinstance(ssl, dict) and "NONE" in ssl.get("__const__", ""):
|
||||
s[key]["verify_tls"] = False
|
||||
dav("CALDAV", "caldav")
|
||||
dav("CARDDAV", "carddav")
|
||||
if isinstance(d.get("CARDDAV_GAL_PATH"), str):
|
||||
s["carddav"]["gal_path"] = d["CARDDAV_GAL_PATH"]
|
||||
if isinstance(d.get("CARDDAV_GAL_MIN_LENGTH"), int):
|
||||
s["carddav"]["gal_min_length"] = d["CARDDAV_GAL_MIN_LENGTH"]
|
||||
if isinstance(d.get("CARDDAV_DEFAULT_PATH"), str):
|
||||
s["carddav"]["default_collection"] = d["CARDDAV_DEFAULT_PATH"].rstrip("/").rsplit("/", 1)[-1]
|
||||
|
||||
# sync / ping / provisioning
|
||||
if isinstance(d.get("SYNC_MAX_ITEMS"), int):
|
||||
s["sync"]["max_items"] = d["SYNC_MAX_ITEMS"]
|
||||
ft = d.get("SYNC_FILTERTIME_MAX")
|
||||
if isinstance(ft, dict) and ft.get("__const__") in FILTER:
|
||||
s["sync"]["filter_time_max"] = FILTER[ft["__const__"]]
|
||||
sc = d.get("SYNC_CONFLICT_DEFAULT")
|
||||
if isinstance(sc, dict):
|
||||
s["sync"]["server_wins"] = "OVERWRITE_PIM" in sc["__const__"]
|
||||
for zk, sec, ak in [("PING_INTERVAL", "ping", "check_interval_secs"), ("PING_LOWER_BOUND_LIFETIME", "ping", "lifetime_min"),
|
||||
("PING_HIGHER_BOUND_LIFETIME", "ping", "lifetime_max"), ("RETRY_AFTER_DELAY", "sync", "retry_after_secs"),
|
||||
("SEARCH_WAIT", "sync", "search_wait_secs"), ("SEARCH_MAXRESULTS", "sync", "search_max_results")]:
|
||||
if isinstance(d.get(zk), int) and not isinstance(d.get(zk), bool):
|
||||
s[sec][ak] = d[zk]
|
||||
if isinstance(d.get("SYNC_CONTACTS_MAXPICTURESIZE"), int):
|
||||
s["carddav"]["max_picture_kb"] = max(1, d["SYNC_CONTACTS_MAXPICTURESIZE"] // 1024)
|
||||
if "PROVISIONING" in d:
|
||||
s["provisioning"]["enabled"] = bool(d["PROVISIONING"])
|
||||
if "LOOSE_PROVISIONING" in d:
|
||||
s["provisioning"]["loose"] = bool(d["LOOSE_PROVISIONING"])
|
||||
if pol_files:
|
||||
pols = parse_policies(pol_files[0])
|
||||
if pols:
|
||||
s["provisioning"]["policies"] = pols
|
||||
s["provisioning"]["default_policy"] = "default" if any(p["name"] == "default" for p in pols) else pols[0]["name"]
|
||||
notes.append(f"imported {len(pols)} polic{'y' if len(pols) == 1 else 'ies'} from {pol_files[0]}")
|
||||
if isinstance(d.get("ZPUSH_HOST"), str) and d["ZPUSH_HOST"]:
|
||||
s["autodiscover"]["server_url"] = f"https://{d['ZPUSH_HOST']}/Microsoft-Server-ActiveSync"
|
||||
|
||||
s = {k: v for k, v in s.items() if v}
|
||||
for n in notes:
|
||||
print(f"# {n}", file=sys.stderr)
|
||||
print(json.dumps(s, indent=2, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user