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

+46
View File
@@ -0,0 +1,46 @@
# Panel API
Every endpoint except `/api/session`, `/api/login`, `/api/setup`, `/metrics` and `/healthz` requires a session
(cookie `ap_session`); methods other than GET additionally need the `X-CSRF-Token` header (value from
`/api/session`).
| Method | Path | Description |
|---|---|---|
| GET | `/api/session` | session state, CSRF token, whether setup is required |
| POST | `/api/setup` | `{token, username, password}` - first administrator |
| POST | `/api/login` / `/api/logout` | sign in / out |
| POST | `/api/password` | `{current, new}` - change password (revokes other sessions) |
| POST | `/api/sessions/revoke-others` | sign out other sessions |
| GET | `/api/overview` | counters (`devices_*` = physical devices, `users_*` = mailboxes, `partnerships_total`), commands, runtime, database size |
| GET | `/api/timeseries?minutes=60` | minute buckets (up to 24 h) |
| GET | `/api/stats/hourly?hours=168` | hourly statistics from the database |
| GET | `/api/live` | requests in flight, push connections, recent events |
| GET | `/api/events` | Server-Sent Events: `request` event after every request |
| GET | `/api/devices`, `/api/devices/{id}` | partnerships (device x mailbox), details (folders, history) |
| POST | `/api/devices/{id}/{action}` | `wipe`, `cancel-wipe`, `resync`, `reprovision`, `block`, `unblock`, `approve`, `remove`, `notes`, `recovery-password` |
| GET | `/api/users` | mailboxes with device names and other mailboxes on the same device |
| GET/PUT | `/api/settings` | settings (secrets as `__SET__`; PUT accepts a full or partial document) |
| POST | `/api/test/{imap,smtp,caldav,carddav}` | `{login, password, settings?}` - connection test |
| GET | `/api/logs?level=WARN&q=text&n=500` | log buffer |
| GET | `/api/audit` | audit log |
| GET/POST | `/api/lockouts`, `/api/lockouts/unlock` | login lockouts |
## Health check
`GET /healthz` on both listeners returns `ok` (HTTP 200) when the process and the database respond, otherwise
HTTP 503.
## Prometheus
Set `panel.prometheus_token`, then:
```yaml
- job_name: active-push
metrics_path: /metrics
authorization: { type: Bearer, credentials: "<token>" }
static_configs: [{ targets: ["127.0.0.1:9091"] }]
```
Metrics: `activepush_requests_total`, `errors_total`, `auth_failures_total`, `bytes_in/out_total`,
`items_in/out_total`, `mails_sent_total`, `pings_active`, `imap_pool_connections`, `uptime_seconds`,
`resident_memory_bytes`, `command_total{command=...}`.
+73
View File
@@ -0,0 +1,73 @@
# Configuration
Runtime settings live in SQLite (`settings` table, JSON with a revision number) and are edited in the panel.
Saving in the panel applies them **immediately** (no restart): the IMAP pool and login cache are cleared and DAV
HTTP clients are rebuilt.
From the command line:
```bash
active-push config get # secrets masked as "__SET__"
active-push config get --with-secrets
active-push config validate file.json
active-push config apply file.json # partial JSON - only the given keys change (deep merge)
```
Startup parameters (listen addresses, data directory, panel TLS) come from `/etc/active-push/environment` or
flags (`active-push --help`).
| Variable | Default | Meaning |
|---|---|---|
| `ACTIVE_PUSH_DATA_DIR` | `/var/lib/active-push` | database directory |
| `ACTIVE_PUSH_EAS_LISTEN` | `127.0.0.1:8081` | ActiveSync + Autodiscover + `/healthz` |
| `ACTIVE_PUSH_ADMIN_LISTEN` | `127.0.0.1:9091` | panel |
| `ACTIVE_PUSH_ALLOW_REMOTE_ADMIN` | `false` | allow the panel on a non-loopback address |
| `ACTIVE_PUSH_ADMIN_TLS_CERT/KEY` | - | panel TLS |
| `ACTIVE_PUSH_EAS_TLS_CERT/KEY` | - | ActiveSync TLS (without a reverse proxy) |
| `ACTIVE_PUSH_LOG_JSON` | `false` | JSON logs |
| `RUST_LOG` | `active_push=info,warn` | log filter |
| `ACTIVE_PUSH_PANEL_LOG` | `active_push=info,warn` | filter for the panel log viewer |
## Z-Push equivalents
| Z-Push | ActivePush (`section.key`) |
|---|---|
| `TIMEZONE` | `general.default_timezone` |
| `USE_FULLEMAIL_FOR_LOGIN` | `general.login_format` (`as_given` / `local_part` / `full_email`) + `general.default_domain` |
| `IMAP_DEFAULTFROM` | `general.email_template` (`{login}`, `{local}@{domain}`, `{local}@example.com`) |
| `IMAP_SERVER`, `IMAP_PORT`, `IMAP_OPTIONS` | `imap.host`, `imap.port`, `imap.security` (`none`/`start_tls`/`tls`), `imap.verify_tls` |
| `IMAP_FOLDER_*`, `IMAP_FOLDER_PREFIX` | `imap.folder_*`, `imap.folder_prefix`, `imap.use_special_use` |
| `IMAP_EXCLUDED_FOLDERS` | `imap.excluded_folders` (list, `prefix*` allowed) |
| `IMAP_AUTOSEEN_ON_DELETE`, `IMAP_INLINE_FORWARD` | `imap.autoseen_on_delete`, `imap.inline_forward` |
| `IMAP_SMTP_METHOD`, `IMAP_SMTP_PARAMS` | `smtp.method`, `smtp.host`, `smtp.port`, `smtp.security`, `smtp.auth` |
| `CALDAV_*` | `caldav.enabled`, `caldav.base_url`, `caldav.path`, `caldav.include_regex`, `caldav.exclude_regex` |
| `CARDDAV_*`, `CARDDAV_GAL_PATH`, `CARDDAV_GAL_MIN_LENGTH` | `carddav.*`, `carddav.gal_path`, `carddav.gal_min_length` |
| `SYNC_MAX_ITEMS`, `SYNC_FILTERTIME_MAX`, `SYNC_CONFLICT_DEFAULT` | `sync.max_items`, `sync.filter_time_max`, `sync.server_wins` |
| `SYNC_CONTACTS_MAXPICTURESIZE` | `carddav.max_picture_kb` |
| `PING_INTERVAL`, `PING_*_BOUND_LIFETIME` | `ping.check_interval_secs`, `ping.lifetime_min/max` |
| `RETRY_AFTER_DELAY`, `SEARCH_WAIT`, `SEARCH_MAXRESULTS` | `sync.retry_after_secs`, `sync.search_wait_secs`, `sync.search_max_results` |
| `PROVISIONING`, `LOOSE_PROVISIONING`, `policies.ini` | `provisioning.enabled`, `provisioning.loose`, `provisioning.policies[]`, `provisioning.user_policies[]` |
| `LOGAUTHFAIL`, `LOGUSERLEVEL` | `security.log_auth_failures`, `logging.debug_users` |
| `ZPUSH_HOST` (autodiscover) | `autodiscover.server_url` |
| `STATE_MACHINE`, `IPC_PROVIDER` | not needed - states in SQLite, IPC in process |
DAV paths accept `%u` (login), `%l` (local part) and `%d` (domain).
## Settings without a Z-Push counterpart
- `security.auth_fail_max / auth_fail_window_secs / lockout_secs` - password guessing protection.
- `security.trusted_proxies`, `security.real_ip_header` - real client IP behind a proxy.
- `security.eas_allowed_networks`, `panel.allowed_networks` - network allow lists.
- `security.allowed_users`, `blocked_users`, `blocked_device_types`, `require_device_approval`.
- `security.auth_cache_secs` - successful login cache (HMAC with a random per-process key, never the password).
- `smtp.enforce_from` - sender always equals the account address.
- `sync.loop_detection` - detects devices replaying an old SyncKey (shrinks the window, reported in the panel).
- `imap.pool_*` - pool of authenticated IMAP connections reused between requests.
- `panel.prometheus_token`, `panel.device_inactive_days`, `logging.*` - see [LOGS.md](LOGS.md).
## Time zones
Devices send the zone as a 172-byte Windows `TIME_ZONE_INFORMATION` structure. ActivePush maps it to an IANA
zone, preferring `general.default_timezone` (comparing this year's DST rules). Recurring events are stored in
CalDAV with `TZID` and a generated `VTIMEZONE`; single events in UTC (unless the original used `TZID`, in which
case the format is kept).
+78
View File
@@ -0,0 +1,78 @@
# Development
## Architecture
```
src/
main.rs CLI (serve, admin, config, device, probe)
lib.rs public router (ActiveSync, Autodiscover, /, /healthz)
app.rs application state: settings (ArcSwap), IMAP pool, metrics, lockouts
config.rs settings model (serde, validation, merging, secrets)
db.rs SQLite (sqlx, WAL), migrations in migrations/, housekeeping
server.rs HTTP/1.1 + HTTP/2 listener, optional TLS, graceful shutdown
wbxml/ WBXML encoder/decoder + MS-ASWBXML code pages
eas/ ActiveSync protocol
mod.rs request parsing (plain/base64 query), auth, provisioning, dispatcher
state.rs SyncKey {uuid}N and states (item fingerprints)
folders.rs FolderSync / Create / Update / Delete
sync.rs Sync, GetItemEstimate, MoveItems
ping.rs Ping and the shared wait-for-changes loop (hanging Sync)
provision.rs policies, remote wipe
mailcmd.rs SendMail / SmartReply / SmartForward / MeetingResponse
itemops.rs ItemOperations, GetAttachment
misc.rs Settings, Search, ResolveRecipients
convert/ RFC 5322 / iCalendar / vCard <-> ApplicationData
backend/
imap.rs own async IMAP client (literals, UIDPLUS, MOVE, SPECIAL-USE)
dav.rs CalDAV/CardDAV client
smtp.rs SMTP (lettre) / sendmail
ical.rs iCalendar/vCard parser and serializer
tz.rs EAS <-> IANA time zones, dates
mime.rs MIME building (SmartReply/Forward)
mod.rs session: folder hierarchy, fingerprint listings, operations
autodiscover.rs
admin.rs panel API, sessions, CSRF, SSE, Prometheus
web/ panel (dependency-free HTML/CSS/JS, embedded in the binary)
```
### Synchronisation
The device state of a folder is a map `item id -> fingerprint` (IMAP: flags; DAV: ETag) stored under the key
`{uuid}N`. Sync compares the backend listing with the state and sends the differences (deletes -> changes ->
adds, newest mail first) within `WindowSize`. The new state is stored as `N+1`; the previous one is kept to
handle lost responses (the device replays the old key). Client adds are idempotent thanks to the `ClientId` map.
Ping keeps a folder signature (IMAP `STATUS UIDNEXT/MESSAGES/UNSEEN/HIGHESTMODSEQ`, DAV CTag); a full listing
comparison only happens when the signature changes.
Collection ids that a device uses but that do not exist (Z-Push leftovers) are remembered per device and sent
as FolderHierarchy Delete on the next FolderSync.
## Development environment
```bash
sudo apt install dovecot-imapd # any dovecot >= 2.3
pip install radicale aiosmtpd
./dev.sh backends # Dovecot :11143, Radicale :15232, SMTP sink :12525
./dev.sh seed && ./dev.sh # ActivePush :8081 / panel :9091
./dev.sh e2e # test of every command against live backends
```
Test accounts: `jan`, `anna`, `biuro`, `kasia` `@test.local`, password `Test-Password-123`.
`./dev.sh lint` - rustfmt, clippy `-D warnings`, JS syntax check and WBXML tag name check
(`scripts/check-wbxml-names.py`). `./dev.sh test` - unit tests (WBXML, IMAP tokenizer, UTF-7, calendar/contact/
task conversions, time zones, query parser, CIDR, MIME).
Protocol debugging: add the login to **ActiveSync -> Logging -> Debug users**; decoded WBXML of requests and
responses goes to the log.
Simulating devices: `active-push probe --device-id F2LX8KQ1N72J --device-type iPhone --model "iPhone 15 Pro" ...`.
## Documentation screenshots
`tools/dev/screenshots.js` (Playwright) regenerates `docs/images/` from a running dev instance:
```bash
node tools/dev/screenshots.js http://127.0.0.1:9091 admin 'password'
```
+132
View File
@@ -0,0 +1,132 @@
# Installation
## Requirements
- Linux x86_64 (the `.deb` needs glibc >= 2.30: Debian 11+, Ubuntu 20.04+). No OpenSSL or PHP - TLS is
rustls and SQLite is built in.
- IMAP server (Dovecot recommended: `UIDPLUS`, `MOVE`, `SPECIAL-USE`, `CONDSTORE`, `UNSELECT`).
- SMTP submission (Postfix on 587 with SASL) or a local `sendmail`.
- Optional CalDAV/CardDAV.
- A TLS reverse proxy (nginx, RustProxy, Apache). ActivePush listens on `127.0.0.1` only by default.
## Debian package
```bash
./dev.sh deb # on the build machine (Rust stable; cargo-zigbuild makes it portable)
sudo apt install ./dist/active-push_*_amd64.deb
```
| Path | Content |
|---|---|
| `/usr/bin/active-push` | binary (server + CLI) |
| `/lib/systemd/system/active-push.service` | service with systemd sandboxing |
| `/etc/active-push/environment` | listen addresses, panel TLS, log level (conffile) |
| `/var/lib/active-push/active-push.db` | SQLite: settings, devices, states, statistics, audit (0600) |
| `/usr/share/doc/active-push/` | documentation and nginx examples |
| `/usr/share/active-push/zpush-import.py` | Z-Push configuration importer |
`postinst` creates the `active-push` system user and enables and starts the service. Upgrading the package
restarts the service (push connections are released and devices reconnect). `apt purge` also removes data.
### Portable builds
`scripts/build-deb.sh` uses `cargo zigbuild` with a glibc 2.31 baseline when it is installed
(`pip install ziglang && cargo install cargo-zigbuild`), otherwise a normal `cargo build`. The `libc6`
dependency is computed from the binary's symbols. `GLIBC_TARGET=0` forces a native build.
### First administrator
```bash
sudo -u active-push active-push admin setup-token # one-time token, valid 60 minutes
ssh -L 9091:127.0.0.1:9091 admin@server # from your workstation
# browser: http://127.0.0.1:9091 -> paste the token, choose login and password (min. 12 characters)
```
Without a browser: `sudo -u active-push active-push admin init --username admin`.
Lost password: `sudo -u active-push active-push admin reset-password` (revokes all sessions).
Always run CLI commands as `active-push`; the binary refuses to touch a data directory owned by another user.
## nginx
Replace the Z-Push fastcgi locations with the snippet from `deploy/nginx-active-push.conf`:
```nginx
location = /Microsoft-Server-ActiveSync {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3660s; # Ping holds the request for up to ~59 minutes
proxy_send_timeout 3660s;
proxy_buffering off;
client_max_body_size 64m;
}
location = /AutoDiscover/AutoDiscover.xml { include snippets/active-push-autodiscover.conf; }
location = /Autodiscover/Autodiscover.xml { include snippets/active-push-autodiscover.conf; }
location = /autodiscover/autodiscover.xml { include snippets/active-push-autodiscover.conf; }
# health check for monitoring / load balancers (no authentication, no data)
location = /activesync-healthz {
proxy_pass http://127.0.0.1:8081/healthz;
access_log off;
}
```
`snippets/active-push-autodiscover.conf` is `deploy/active-push-autodiscover.conf`.
**Important:** `X-Forwarded-For $remote_addr` (overwrite, not append) together with the default trusted proxy
`127.0.0.1/32` gives the real client IP for lockouts and logs.
Port 8081 answers `/` with a short text, `/healthz` with `ok` (also checks the database) and anything else
with a plain 404.
## RustProxy
Create a vhost for the mail domain that routes `/Microsoft-Server-ActiveSync` and the three autodiscover paths
to `http://127.0.0.1:8081` with a response timeout of at least 3660 s and response buffering disabled; point a
health check at `/healthz`. RustProxy sets `X-Forwarded-For`, so its address must be in **Security -> Trusted
proxies** (loopback by default).
## Postfix (submission)
Recommended: SMTP on 587 authenticated with the device user's credentials (`auth: user_credentials`). With
`smtpd_sender_login_maps` + `reject_sender_login_mismatch` the mail server enforces the sender as well.
The `sendmail` method needs `NoNewPrivileges=false` and `ReadWritePaths=/var/spool/postfix/maildrop` in the unit
(setgid `postdrop`), which is why it is not recommended.
## Panel outside localhost
The panel refuses to start on a non-loopback address unless allowed:
```ini
# /etc/active-push/environment
ACTIVE_PUSH_ALLOW_REMOTE_ADMIN=true
ACTIVE_PUSH_ADMIN_LISTEN=0.0.0.0:9091
ACTIVE_PUSH_ADMIN_TLS_CERT=/etc/active-push/tls/fullchain.pem
ACTIVE_PUSH_ADMIN_TLS_KEY=/etc/active-push/tls/privkey.pem
```
and set **Security -> Networks allowed to use the panel**. Certificates must be readable by the
`active-push` group (e.g. copied by a certbot deploy hook).
## Upgrades
Install the new package; the schema is migrated automatically on start. Settings, devices and states are kept.
## Backups
See [LOGS.md](LOGS.md#backups). Losing the database never loses mail - devices resynchronise.
## Building from source without a package
```bash
cargo build --release
sudo install -m 0755 target/release/active-push /usr/bin/active-push
sudo adduser --system --group --home /var/lib/active-push active-push
sudo install -d -o active-push -g active-push -m 0700 /var/lib/active-push
sudo install -m 0644 deploy/active-push.service /etc/systemd/system/
sudo install -d -m 0750 -g active-push /etc/active-push
sudo install -m 0640 -g active-push deploy/environment.example /etc/active-push/environment
sudo systemctl daemon-reload && sudo systemctl enable --now active-push
```
+79
View File
@@ -0,0 +1,79 @@
# Logging, retention and database maintenance
ActivePush is designed so that nothing grows without bounds. This page lists every place where data
accumulates and how it is limited.
## Server log
- Written to **stdout/stderr**, collected by **journald** under the systemd unit. ActivePush does not write
log files itself, so there is nothing to rotate with logrotate.
- Level: `RUST_LOG` in `/etc/active-push/environment` (default `active_push=info,warn`).
`ACTIVE_PUSH_LOG_JSON=true` switches to JSON lines (ELK, Loki, Vector).
- Size is controlled by journald. Recommended drop-in:
```ini
# /etc/systemd/journald.conf.d/size.conf
[Journal]
SystemMaxUse=500M
SystemKeepFree=1G
MaxRetentionSec=30day
```
then `systemctl restart systemd-journald`. Read the log with `journalctl -u active-push -f`.
- Volume at `info` is roughly one line per notable event (new device, provisioning, failed login, sent
mail, errors) - routine Sync/Ping requests are not logged individually.
- **Debug WBXML** (`logging.log_wbxml` or `logging.debug_users`) logs full requests and responses including
message bodies. Enable it only for a short diagnosis and for chosen mailboxes.
If you redirect output to a file yourself (e.g. a container without journald), rotate it with
`copytruncate`:
```
/var/log/active-push/active-push.log {
daily
rotate 14
compress
missingok
notifempty
copytruncate
}
```
## In-memory buffers (panel)
| Buffer | Limit |
|---|---|
| Log viewer | `logging.ring_size` lines (default 2000, 100..100000) |
| Live request stream | last 500 requests |
| Minute statistics | 24 hours (1440 buckets) |
| Active mailbox tracking | entries older than 24 h are pruned every minute |
| Failed login counters / lockouts / login cache | expire automatically, pruned every minute |
| Loop detection, stale collections, time zone cache | bounded and expiring |
All of these are lost on restart by design; persistent history is in the database.
## Database
`/var/lib/active-push/active-push.db` (SQLite, WAL). Hourly housekeeping:
| Data | Retention |
|---|---|
| Hourly statistics | `logging.stats_retention_days` (default 90) |
| Audit log | `logging.audit_retention_days` (default 365) |
| Panel sessions, setup tokens | removed when expired |
| Devices not seen for `panel.device_inactive_days` (default 180, `0` = never) | removed with their sync states and folder statistics; a device that returns later simply resynchronises |
| Sync states | at most two per folder and device (current + previous for retries); removed with the device, on folder deletion and on FolderSync reset; states untouched for more than 400 days are dropped |
| Folder/item id maps | removed for mailboxes that no longer have any device |
| WAL file | checkpointed and truncated every hour |
| Free pages | incremental auto-vacuum (databases from 0.1.0 are converted once automatically) |
Typical size: a few hundred kilobytes per device, mostly sync states of large mail folders. The panel shows
the database and WAL size on the Overview page (**Server** card).
## Backups
```bash
sqlite3 /var/lib/active-push/active-push.db ".backup '/root/active-push-$(date +%F).db'"
```
Losing the database never loses mail data: devices perform a full resynchronisation.
+58
View File
@@ -0,0 +1,58 @@
# Migrating from Z-Push
## 1. Import the configuration
```bash
python3 /usr/share/active-push/zpush-import.py /usr/share/z-push > zpush.json # source installation
python3 /usr/share/active-push/zpush-import.py /etc/z-push > zpush.json # Debian packages (*.conf.php)
less zpush.json # review and complete
sudo -u active-push active-push config apply zpush.json
```
The importer reads `config.php`, `backend/{imap,caldav,carddav,combined}/config.php`, `autodiscover/config.php`
and `policies.ini`. Comments on stderr point out what needs attention (e.g. `IMAP_SMTP_METHOD = 'mail'` ->
sendmail; switching to SMTP submission on 587 is recommended).
## 2. Switch the traffic
1. Install ActivePush, create the administrator, verify the backends with **Test connection** in the panel.
2. Check the protocol with a real account:
`active-push probe --url https://mail.example.com/Microsoft-Server-ActiveSync --user jan@example.com`
(through nginx after the switch) or directly against `http://127.0.0.1:8081/...` before it.
3. In nginx replace the `fastcgi_pass ... z-push/index.php` and `autodiscover.php` blocks with `proxy_pass`
(see INSTALL.md), then `nginx -t && systemctl reload nginx`.
4. Disable the Z-Push PHP-FPM pool and the `z-push-admin` cron jobs.
## 3. What happens to devices
Z-Push states are not transferred (their format depends on the PHP backends). On the first connection:
- the device sends its old `X-MS-PolicyKey` -> new provisioning (HTTP 449, or status 142/144 for AS >= 14),
- the old hierarchy `SyncKey` -> `FolderSync` status 9 -> the device downloads the folder list again,
- old folder keys -> `Sync` status 3 -> full folder synchronisation,
- folder ids that only existed in Z-Push (e.g. `a6271970`) -> `Sync` status 12, and the next FolderSync sends a
**Delete** for them so the device stops using them.
iOS, Android (Gmail, Samsung, Nine), Outlook and Windows Mail handle this automatically. Large mailboxes may
take a few minutes - limit `sync.filter_time_max` or the policy `maxemailagefilter` if needed.
## 4. `z-push-admin` equivalents
| z-push-admin | ActivePush |
|---|---|
| `-a list` | `active-push device list` / panel **Devices** |
| `-a wipe -d ID` | `active-push device wipe <id>` / **Remote wipe** button |
| `-a remove -d ID` | `active-push device remove <id>` |
| `-a resync -d ID` | `active-push device resync <id>` / **Full resync** |
| `-a clearloop` | automatic - loop counter in the device details |
| `-a fixstates` | `panel.device_inactive_days` + hourly housekeeping |
| `z-push-top` | panel **Live** |
## 5. Functional differences
- **Kopano/Zarafa (MAPI)** is not supported - only IMAP/SMTP + CalDAV/CardDAV (`combined`).
- **LDAP/SQL as sender source** (`IMAP_FROM_SQL_*`, `IMAP_FROM_LDAP_*`) is replaced by `general.email_template`.
- **searchldap** -> GAL through CardDAV (`carddav.gal_path`); SOGo/Nextcloud expose the directory as a book.
- **Out of office** - reading returns "disabled", setting returns an error; configure vacation in Sieve.
- **AS 16.x** is not announced (like Z-Push with IMAP by default) - devices use 14.1.
- Notes and SMS are not synchronised (Z-Push IMAP did not do this either).
+109
View File
@@ -0,0 +1,109 @@
# Administration panel
The panel is a separate listener (default `127.0.0.1:9091`) embedded in the binary. Reach it through an SSH
tunnel (`ssh -L 9091:127.0.0.1:9091 server`) or a VPN. It is in English by default; switch to Polish with the
language selector (remembered per browser). Settings pages apply changes immediately, without a restart.
## First login
![First run](images/setup.png)
On the first start there is no administrator. Generate a one-time token on the server and paste it together
with the new login and password (at least 12 characters):
```bash
sudo -u active-push active-push admin setup-token # valid 60 minutes, single use
```
Without a browser: `sudo -u active-push active-push admin init --username admin`.
## 01 Overview
![Overview](images/overview.png)
- **Tiles** - requests per minute, active push connections, **devices** (physical phones, counted by DeviceId),
**mailboxes** (logins), mails sent, errors, failed logins and transfer. One person with several mailboxes on
one phone counts as one device and several mailboxes; the devices tile also shows the number of
partnerships (device x mailbox pairs).
- **Charts** - traffic, synchronised items and data per minute (60 min / 6 h / 24 h), requests per hour for the
last 7 days. Hover a chart to read exact values.
- **Commands**, **device types** and **server** (version, uptime, memory, IMAP pool, database and WAL size,
configured backends).
## 02 Live
![Live](images/live.png)
Push connections (Ping or hanging Sync) with their heartbeat progress, requests in flight and a live stream of
finished requests (Server-Sent Events). Filter the stream by mailbox, device or command; pause it while reading.
## 03 Devices
![Devices](images/devices.png)
One row per **physical device**. Every mailbox configured on it is a chip; a green dot means that mailbox
currently holds a push connection. Filters: push, pending approval, blocked, inactive for 30 days.
Click a mailbox chip (or the row) to open the partnership:
![Device](images/device.png)
- details reported by the device (model, OS, IMEI, user agent, protocol, policy),
- per-folder statistics (items, sent/received, last sync, window size, loop detection hits),
- recent requests and free-form notes,
- actions: **approve**, **full resync**, **force re-provision**, **block/unblock**, **remote wipe** (confirmed by
typing the device id), **cancel wipe**, **show recovery password**, **remove partnership**.
All actions are written to the audit log.
## 04 Mailboxes
![Mailboxes](images/mailboxes.png)
Every login that synchronises through ActiveSync, with its devices, the **other mailboxes on the same device**
(useful to see one person's accounts together), push connections, requests, last activity and effective policy.
Blocking a mailbox applies immediately on every device. Active login lockouts can be cleared here.
## 05 Backends
![Backends](images/backends.png)
IMAP, SMTP, CalDAV and CardDAV. Each section has a **Test connection** button that uses the values currently in
the form (also unsaved ones) and an optional test login; the password is never stored.
## 06 ActiveSync
![ActiveSync](images/activesync.png)
Login format and default domain, e-mail address template, protocol version, synchronisation limits, push
intervals, Autodiscover, logging and retention.
## 07 Policies
![Policies](images/policies.png)
Provisioning switch, loose provisioning, named policies with every `policies.ini` option and per-mailbox
assignments (`login=policy`, one per line). Changing a policy makes devices accept it again.
## 08 Security
![Security](images/security.png)
Login lockout thresholds, trusted proxies and client IP header, network allow lists for ActiveSync and the
panel, allowed/blocked mailboxes and device types, device approval, panel session lifetime, Prometheus token,
administrator password change and signing out other sessions.
## 09 Logs and audit
![Logs](images/logs.png)
Recent server log lines (level filter, search, auto refresh) and the audit trail of administrative actions.
The log view is an in-memory ring buffer; the full log goes to journald. See [LOGS.md](LOGS.md).
## Themes and small screens
![Light theme](images/overview-light.png)
The theme button switches between dark and light. On narrow screens the navigation collapses into a menu.
<img src="images/mobile.png" alt="Mobile" width="320">
+61
View File
@@ -0,0 +1,61 @@
# Security
## Model
- **Devices** authenticate with Basic Auth (required by the protocol) - always behind TLS (reverse proxy).
The password is verified by logging in to IMAP and is passed to SMTP/DAV only within the same request. It is
never stored. The successful login cache keeps only HMAC-SHA256(login, password) with a random key generated
at process start.
- **The IMAP pool** keeps authenticated connections keyed by the same HMAC - a connection is only reused for a
request with identical credentials.
- **The panel** is a separate listener (loopback by default; another address requires explicit consent).
Administrator passwords: argon2id. Session: random 256-bit token in an `HttpOnly; SameSite=Strict` cookie
(`Secure` over HTTPS); only its SHA-256 is stored. Every modifying request needs the CSRF header. The first
administrator can only be created with a one-time token from the CLI (valid 60 minutes).
- **Panel headers:** strict CSP (`script-src 'self'`, no inline code), `X-Frame-Options: DENY`, `nosniff`,
`Referrer-Policy: no-referrer`, COOP.
## Abuse protection
- Lockout after `auth_fail_max` failures per account and `3x` per IP within the window; HTTP 429 with
`Retry-After`. Other accounts from the same IP keep working until the IP limit is reached.
- fail2ban friendly log line:
`authentication failed user=jan@example.com ip=203.0.113.7 service=activesync`
```ini
# /etc/fail2ban/filter.d/active-push.conf
[Definition]
failregex = authentication failed user=\S+ ip=<HOST> service=
journalmatch = _SYSTEMD_UNIT=active-push.service
```
- Panel: 5 failed logins in 10 minutes lock the IP for 15 minutes; constant verification cost even for unknown
accounts.
- Limits: request size (`sync.max_request_mb`), attachments, messages, WBXML depth and element count, IMAP
response size, timeouts on every backend operation, HTTP header read timeout.
- Unknown ActiveSync command names are counted as `Unknown` (no unbounded metric labels).
- XML (Autodiscover, WebDAV) is parsed without DTD/entities (XXE, billion laughs).
- The DAV client never follows `href`s to another host (SSRF).
- Logins, device ids and device types are validated; SQL parameters are always bound.
- `smtp.enforce_from` rewrites `From` to the account address; `Bcc` is stripped from the transmitted copy.
## Device control
- Provisioning policies (password, encryption, camera, maximum mail age ...). Changing a policy invalidates
keys - devices must accept the policy again.
- Remote wipe (confirmed by typing the device id), device block, approval of new devices, mailbox block,
blocked device types/User-Agents.
- Every administrative action is written to the audit log (who, what, when, from where).
## systemd
The service runs as `active-push` without privileges or capabilities, with `ProtectSystem=strict`,
`ProtectHome`, `PrivateTmp`, `PrivateDevices`, `MemoryDenyWriteExecute`, a system call filter and write access
only to `/var/lib/active-push`. The database is `0600`, the directory `0700`, `UMask=0077`.
## Recommendations
1. TLS only at the reverse proxy, with HSTS; ActivePush on `127.0.0.1`.
2. Panel through an SSH tunnel or VPN. If exposed - TLS + `panel.allowed_networks`.
3. SMTP submission with `reject_sender_login_mismatch`.
4. Enable `security.require_device_approval` in company environments.
5. Set a Prometheus token only if you scrape metrics; the endpoint then lives on the panel port.
6. `logging.log_wbxml` contains message content - only for short diagnosis.
+52
View File
@@ -0,0 +1,52 @@
# Troubleshooting
## Health and diagnostics
```bash
curl -s http://127.0.0.1:8081/healthz # "ok" when the process and database respond
journalctl -u active-push -f # server log
sudo -u active-push active-push device list
active-push probe --url https://mail.example.com/Microsoft-Server-ActiveSync --user jan@example.com
```
`probe` performs OPTIONS, Provision, FolderSync and an Inbox Sync like a phone would. Use `--device-id`,
`--device-type` and `--model` to imitate a specific device. For protocol-level detail add the mailbox to
**ActiveSync -> Logging -> Debug users**; decoded WBXML of every request/response then appears in the log.
## After migrating from Z-Push: "collection ... not found" in the live stream
Phones may keep folder ids created by Z-Push (e.g. `a6271970`). ActivePush answers such a Sync with status 12
(hierarchy changed), remembers the id and sends a FolderHierarchy **Delete** for it on the device's next
FolderSync, so the phone drops the phantom folder and continues with the real ones. If a device still loops
after a few minutes, use **Full resync** in the device dialog or remove and re-add the account on the phone.
## Device keeps asking for the password / HTTP 401
- Wrong password, or the login format does not match IMAP (`general.login_format`, `default_domain`).
- The account or IP may be locked out: **Mailboxes -> Active login lockouts** shows and clears lockouts.
- Check `authentication failed user=... ip=...` lines in the log.
## HTTP 449 or status 142/144
The device has not accepted the current policy. It normally re-provisions automatically. If a device cannot
apply the policy (e.g. it does not support encryption), relax the policy or enable loose provisioning.
## Push is late
Push latency is at most `ping.check_interval_secs` (default 30 s). The reverse proxy must allow long requests
(`proxy_read_timeout 3660s`, buffering off). The **Live** page shows every held Ping and its heartbeat.
## HTTP 503 with Retry-After
The IMAP server was unreachable during the request. Check the **Backends** page test buttons.
## Panel is not reachable
The panel listens on loopback by default; use `ssh -L 9091:127.0.0.1:9091 server`. Starting it on another
address requires `ACTIVE_PUSH_ALLOW_REMOTE_ADMIN=true` (preferably with TLS). Lost password:
`sudo -u active-push active-push admin reset-password`.
## "belongs to uid ..." when running a command
CLI commands must run as the service user so database files keep the right owner:
`sudo -u active-push active-push ...`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB