first commit

This commit is contained in:
Mateusz Gruszczyński committed 2026-11-05 12:52:32 +01:00
commit 19dfc51360
85 files changed
+25504

No files matched your search

+73
View File
@@ -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();
})();
+149
View File
@@ -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
+10
View File
@@ -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 }
}