Compare commits

..

12 Commits

Author SHA1 Message Date
25e1e89ed1 feat(common): radar_concentric painter + phase-aware dashboards (ref #138)
Port the radar concentric layout from remote-ui/round/agent/display/fallback/
into a stateless painter under secubox_common.painters.radar_concentric.
Both round and square dashboards now consume it; the square's main loop
drives a monotonic-clock phase so its left half rotates the same way the
round's deployed fallback_manager does.

What landed
-----------
- `secubox_common/painters/radar_concentric.py`
  - `paint(img, center, modules, metrics, radii=None, phase=0.0, ...)`
  - phase × 2π = sweep angle radians, clockwise from 12 o'clock.
  - Module → wheel-angle map decoupled from list order via
    DEFAULT_NAME_TO_ANGLE so AUTH/WALL/ROOT/MESH/MIND/BOOT sit at the
    rainbow position matching their colour regardless of MODULES order.
  - DEFAULT_RADII = [214, 188, 162, 136, 110, 84] matches the deployed
    fallback_manager.py geometry.
  - draw_hub=False lets the converged dashboard composite its own
    central button + pod cluster on top.

- `DashboardCanvas.paint_radar_concentric` — thin wrapper, single source
  of truth verified by a test that asserts canvas-vs-painter byte-equal.

- `RoundDashboard.layout(metrics, phase=0.0)` and
  `SquareDashboard.layout(metrics, phase=0.0)` — phase=0 preserves the
  static still-frame contract for tests / callers that don't drive
  animation. RING_RADII bumped to the deployed [214..84] geometry on
  both classes so the converged left-half look matches the round.

- `secubox_eye_square_kiosk.__main__` drives
  `phase = (time.monotonic() * RADAR_RPM / 60) % 1` at 12 RPM, matching
  fallback_manager._sweep_speed.

Tests: 8 new in secubox_common/tests/test_radar_concentric.py — phase
differentiation, period-1 wrap, default-radii fallback, length-mismatch
guard, hub toggle, sweep_accent range, canvas-helper byte-equal.

Total green: 118 / 118 (36 secubox_common + 78 square kiosk + 4 round).

Out of scope for this commit (deferred)
---------------------------------------
- `fallback_manager.py` migration to the painter (#138 acceptance lists it).
  fallback_manager uses its own brighter MODULES list and stateful
  ConnectionState machinery; converting it requires a colour-palette
  decision the user has not yet signed off on. Will land in a follow-up
  fixup after hardware confirms the square radar visual is what we want.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:28:09 +02:00
CyberMind
b0b42e81de
Feature/135 converge round square dashboards into re (#140)
* fixup(common): pod_size=48 to match deployed icon sizes (ref #135)

Module icons were falling back to first-letter placeholders because
SquareDashboard/RoundDashboard called paint_pod_cluster(pod_size=40)
while load_module_icon only ships sizes 22/48/96/128. Bump pod_size
to 48 and radius from 70 to 78 so the pod inner edge (54) stays clear
of the central button (44).

Confirmed on Pi 4B square hardware: pods now render with full icons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fixup(square): ship remote-ui/common/assets/icons to /var/www/common/assets (ref #135)

The square build script shipped remote-ui/common/python/ but skipped
remote-ui/common/assets/. secubox_common.icons.load_module_icon resolves
icons at /var/www/common/assets/icons/ — when that path is missing on the
deployed image, every module pod falls back to a first-letter placeholder.

The round build script already does this at line 925; bringing square to
parity. Fails the build (exit 2) if no *-48.png icons land — same
shape as the existing python-package guard right above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fixup(square): install secubox-otg-gadget.sh to /usr/local/sbin (ref #135)

The square build script enables secubox-otg-gadget.service but never copies
the composer script to /usr/local/sbin/. On boot the service ExecStarts a
missing file → fails silently → the Pi 4B's USB-C bus stays unconfigured →
MOCHAbin/host see no enumeration (zero dmesg events, xhci setup timeouts).

The round build copies it at build-eye-remote-image.sh:618; bringing square
to parity. The script itself lives in remote-ui/common/shell/, already
shared between the two images.

Hardware-confirmed: Pi 4B booted and rendered the dashboard but MOCHAbin
got no USB events whatsoever on either a hub-deep port or a direct port,
across cable swaps. Root cause was the missing composer, not cabling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:26:41 +02:00
CyberMind
355767935c
Round image: drop dead ifupdown config, give secubox sudo, fix misleading OTG comment (closes #139) (#143)
* fix(round): remove dead ifupdown config + secubox sudo + clarify gadget IP comment (closes #139)

Three small cleanups against the round image, all triggered by the
follow-up to PR #137's OTG bench:

1. Drop `remote-ui/round/files/etc/network/interfaces.d/usb0` — the
   stanza configured `usb0` via `ifupdown`, but `ifupdown` is not in
   the apt list, `networking.service` is inactive, and the actual IP
   binding happens in `secubox-otg-gadget.sh` (which configures `usb1`
   programmatically with `ip addr add`). Keeping the file on disk
   misleads diagnostics — it cost a round trip during the 2026-05-15
   bench when I assumed `usb0`/10.55.0.2 must be where the IP would
   land. Deleting it.

2. Add `sudo` to the `secubox` user's group list in
   `build-eye-remote-image.sh`. Without it, ACM serial console
   recovery is impossible — the user can log in but can't fix
   anything. The round previously needed `pi/raspberry` (which the
   build script does not provision) as a fallback; now `secubox` can
   recover networking from the only path that survives a broken IP
   link.

3. Rewrite the misleading comment block in `secubox-otg-gadget.sh`
   about which kernel interface name belongs to which gadget function.
   The legacy comment claimed `usb1 = ECM` and tied the binding choice
   to "Linux uses cdc_ether"; in reality both RNDIS and ECM share the
   same host_addr so the host can reach 10.55.0.2 via either function
   (verified on Pop!_OS 22.04 host: ping succeeded with 10.55.0.1
   bound on the host's RNDIS leg). Comment now states the actual
   contract: bind on `usb1` (the second-registered network function),
   independent of which transport carries the packets.

Test plan
---------

- [ ] Rebuild the round image and flash to Pi Zero W 1st gen.
- [ ] Verify boot still reaches OFFLINE-mode radar on the HyperPixel.
- [ ] `id secubox` on the booted device shows `sudo` in groups.
- [ ] `ls /etc/network/interfaces.d/usb0` returns "no such file".
- [ ] `systemctl is-active secubox-otg-gadget` returns `active`.
- [ ] Host ping 10.55.0.2 succeeds (regression check — the comment
      rewrite is documentation-only, must not change behaviour).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(round): also drop inline heredoc that recreates dead ifupdown stanza (ref #139)

The previous commit deleted remote-ui/round/files/etc/network/interfaces.d/usb0
but missed that build-eye-remote-image.sh writes the same file inline at
build time via heredoc. Without removing that block too, the dead config
keeps shipping to the rootfs.

Verified on the just-rebuilt image: file still present after first
attempt at the fix. With both source-tree deletion AND the heredoc gone,
the config never lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:26:32 +02:00
CyberMind
3219050775
Mail stack: Phase 1 — source-catch-up + legacy package cleanup (#141)
* test(mail): phase1 scaffolding — lib/ stubs + bats red baseline (ref #136)

* feat(mail): lib/lxc.sh — unprivileged veth helpers + bats assertions (ref #136)

* feat(mail): lib/migrate.sh — legacy LXC/toml detectors + I13 data guard (ref #136)

* feat(mail): lib/install.sh — extract Postfix/Dovecot/Roundcube helpers (ref #136)

* feat(mail): mailctl 2.2.0 — canonical paths/IP + migrate-config (ref #136)

* feat(mail): mail.toml schema for single LXC + canonical paths (ref #136)

* fix(mail): API reads canonical config keys; legacy aliases retained (ref #136)

* feat(mail): mailserverctl + roundcubectl reduced to deprecation shims (ref #136)

* feat(mail): mail-migrate-to-single-lxc.sh — defensive scanner (ref #136)

* feat(mail): postinst runs migrate-config + scanner on upgrade <2.2 (ref #136)

* feat(mail): deprecate 3 nginx snippets + WAF integration NEWS (ref #136)

* feat(mail): HAProxy mail-TCP snippet targeting 10.100.0.10 (ref #136)

* feat(mail): bump to 2.2.0 with Breaks/Replaces (ref #136)

* feat(mail): mark mail-lxc/webmail-lxc/webmail transitional 2.2.0 (ref #136)

* test(mail): phase1 endpoint presence pytest (62 routes) (ref #136)

* test(mail): phase1 end-to-end acceptance smoke (ref #136)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-05-15 13:26:21 +02:00
c39743726a docs(wip): expand #135/PR #140 with this session's fixups + hardware bench
- List the three fixup commits landed on feature/135-* today (pod_size,
  icon assets, gadget composer) so the WIP captures what's actually in
  the PR head right now (89968477, 35 commits ahead, net +35 / -2086).
- Add hardware-bench notes for both round (Pi Zero W) and square (Pi 4B
  + DSI) covering the converged work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:22:04 +02:00
5c72d869a4 docs(wip): housekeeping — 3 stale 127-* worktrees cleaned, master pushed
- Removed feature/127-add-remote-ui-square-variant-for-pi-4b-7 (PR #130 merged)
- Removed feature/127-phase2-square-variant (PR #131 closed, superseded)
- Force-removed feature/127-phase3-python-kiosk (PR #132 merged) — stray Signal
  Desktop apt-key files in the worktree were unrelated to the project, safe to
  discard
- Master synced with origin at a313816e (pushed 839bab94..a313816e)
- Updated section with the table of still-active worktrees for at-a-glance view

Note: agent-worktree.sh clean resolves by issue number which collides for
multi-worktree issues like #127 — used direct git worktree remove + branch -D
instead. Possible enhancement for the script.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:19:44 +02:00
a313816edc docs(wip): hardware bench results for #138/#139 + rescope of #139 (closes nothing)
- #138 (PR #142, radar_concentric): mark hardware-validated on Pi 4B+DSI
  (rotating radar + icons + right panel) on 2026-05-15.
- #139 (PR #143, round image cleanup): rescope from "OTG dead" to
  dead-ifupdown / sudo / misleading-comment cleanup; OTG was always
  working on usb1 via secubox-otg-gadget.sh. Bench validated all 3 fixes
  live (Pi Zero W ARMv6 + HyperPixel) including host ping 3/3 at 0.3 ms,
  SSH OPEN, secubox in sudo group.
- Note the orthogonal pre-existing Pi Zero W dwc2 panic under xHCI reset
  hammering, deferred for a separate investigation.

Conflict between local stash and divergent master resolved by taking the
HEAD side; stashed simpler version dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
9c6e11d54d docs: WIP snapshot 2026-05-15 — mail #136 PR #141, dashboard #135 PR #140, #138 PR #142, #139 PR #143
- Phase 3 (#127): 3 of 4 hardware gates closed (Pi 4B / Pi 400 / Pi Zero W);
  only Task 18 diffoscope regression remaining. 5 bench bugs fixed in PR #134.
- Mail stack Phase 1 (#136): mailctl/mailser + 62-route pytest + acceptance
  smoke landed; PR #141 opened today.
- Converged dashboard (#135): PR #137 merged (839bab94); PR #140 follow-on
  opened today.
- New: #138 radar_concentric → secubox_common (PR #142), #139 round usb0 OTG
  networking bug (PR #143).
- CMSD SPDX (#81): worktree at aa1f7481, no PR yet.
- Note 3 stale 127-* worktrees safe to clean; master 4↑/1↓ vs origin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
3770586a31 docs: Phase 1 rollback recipe (pre-issue) 2026-05-15 13:16:16 +02:00
bd4ef55a98 docs: revise mail Phase 0 spec + Phase 1 plan to match board reality
Live board inspection on 2026-05-15 found that the single-mail-LXC layout
has already been hand-built under /data/lxc/mail with /data/volumes/mail
bind-mounts and 10.100.0.10/24 unprivileged-veth networking on br-lxc.
The repo source code was out of date (still referenced /srv/lxc, /srv/mail,
192.168.255.x, mail_container/webmail_container).

Spec rev. 2:
- Invariants now reflect /var/lib/lxc/mail, /data/volumes/mail, 10.100.0.10
- New invariant I12: persistent data lives on host bind-mounts only
- New invariant I13: existing 5-user secubox.in mailboxes MUST be preserved
- Postgrey dropped entirely; ClamAV deferred to Phase 2 with Rspamd

Plan rev. 2:
- Reduced from ~30 tasks to ~15 — Phase 1 is now "catch source up to board"
- Migration script becomes defensive scanner (no destructive ops)
- guard_data_path bats test enforces the data-preservation invariant
- secubox-mail bumps 2.1.0 → 2.2.0 (was incorrectly targeting 2.0.0 in rev. 1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
b59d053c54 docs: Phase 1 implementation plan (mail stack LXC consolidation)
Bite-sized TDD plan covering 8 milestones (~30 tasks) to collapse the
existing mailserver + roundcube two-LXC layout into a single 'mail' LXC.
Resolves spec open questions: Roundcube via nginx+php-fpm; HAProxy
TCP-pass-through to LXC for 25/587/993/4190. Includes data migration
helper, deprecation of 3 packages to transitional, and end-to-end
acceptance smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
2494b55a18 docs: Phase 0 architecture spec for full-featured mail+webmail stack
8-phase plan to deliver multi-domain Postfix+Dovecot+Rspamd+ClamAV+Roundcube
inside a single 'mail' LXC, integrated with secubox-users (identity) and
secubox-nextcloud (CardDAV/CalDAV). Locks 10 architectural invariants,
deprecates secubox-mail-lxc + secubox-webmail-lxc + secubox-webmail, and
records the deferred per-phase design questions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:16:16 +02:00
47 changed files with 3372 additions and 2607 deletions

View File

@ -1,5 +1,117 @@
# WIP — Work In Progress
*Mis à jour : 2026-05-14*
*Mis à jour : 2026-05-15*
---
## 🔄 2026-05-15: Mail stack Phase 1 — LXC consolidation + source catch-up (Issue [#136](https://github.com/CyberMind-FR/secubox-deb/issues/136), PR [#141](https://github.com/CyberMind-FR/secubox-deb/pull/141) OPEN)
### Objective
Catch the in-repo source up to the test board's `/data/lxc/mail` + `/data/volumes/mail` + `10.100.0.10/24` reality (repo still references `/srv/lxc`, `/srv/mail`, `192.168.255.30`, separate `mail_container`/`webmail_container`). Phase 1 collapses the dual-container layout into a single mail LXC, deprecates legacy `secubox-mail-lxc` / `secubox-webmail` / `secubox-webmail-lxc` companion packages with `Breaks/Replaces`, and ships HAProxy mail-TCP snippets pointed at the new IP.
### Status (worktree `136-mail-stack-phase-1-source-catch-up-legac`, branch `feature/136-mail-stack-phase-1-source-catch-up-legac`)
- Phase 0 spec rev. 2 (`docs/superpowers/specs/2026-05-15-mail-stack-architecture-design.md`) + Phase 1 plan + rollback recipe committed to master
- mailctl/mailser feature commits landed (latest `bade94f1`)
- Versions bumped to 2.2.0 with `Breaks/Replaces` markers on the three legacy packages
- HAProxy mail-TCP snippet targets new `10.100.0.10`
- Test coverage shipped: 62-route endpoint-presence pytest + end-to-end acceptance smoke
- **PR [#141](https://github.com/CyberMind-FR/secubox-deb/pull/141) opened** today — awaiting review + live deploy/cutover on test board
---
## 🔄 2026-05-15: remote-ui converged dashboard (Issue [#135](https://github.com/CyberMind-FR/secubox-deb/issues/135), PR [#140](https://github.com/CyberMind-FR/secubox-deb/pull/140) OPEN)
### Status
- PR [#137](https://github.com/CyberMind-FR/secubox-deb/pull/137) (initial converge round/ + square/ dashboards into `secubox_common` + pointer input on Pi 4B/400) **squash-merged `839bab94`**
- **PR [#140](https://github.com/CyberMind-FR/secubox-deb/pull/140) OPEN** on `feature/135-converge-round-square-dashboards-into-re` — head `89968477`, 35 commits ahead of `origin/master`, net +35 / 2086 lines
- Spec + plan landed on master (`b5e44e72`, `78316556`)
### Fixups added in this session (2026-05-15)
- `387fabb4` `fixup(common): pod_size=48 to match deployed icon sizes` — modules pods were rendering as letter placeholders on Pi 4B because `paint_pod_cluster(pod_size=40)` missed the deployed icon sizes (22/48/96/128); bumped to 48, radius 70→78 for clearance from the central button.
- `f4acd5a9` `fixup(square): ship remote-ui/common/assets/icons` — the square build copied `common/python/` but skipped `common/assets/`; without the icons under `/var/www/common/assets/icons/`, `secubox_common.icons.load_module_icon` would fall back to first-letter placeholders.
- `89968477` `fixup(square): install secubox-otg-gadget.sh to /usr/local/sbin` — the square `secubox-otg-gadget.service` ExecStarts that path; without the composer script the gadget never composed (zero USB enumeration events on the MOCHAbin, xHCI setup timeouts).
### Hardware bench (2026-05-15)
- **Pi 4B + 7" DSI (square):** converged dashboard renders ✓, icons ✓, all 4 right-panel tabs work (alerts/console/module_detail/mode_controls), USB-C OTG composes to MOCHAbin after the gadget-composer fixup landed.
- **Pi Zero W + HyperPixel 2.1 (round):** boots clean, `fallback_manager.py` OFFLINE radar renders correctly on the existing image.
---
## 🔄 2026-05-15: Port `radar_concentric` into `secubox_common` (Issue [#138](https://github.com/CyberMind-FR/secubox-deb/issues/138), PR [#142](https://github.com/CyberMind-FR/secubox-deb/pull/142) OPEN)
### Status
- Issue opened today; PR [#142](https://github.com/CyberMind-FR/secubox-deb/pull/142) opened the same day on `feature/138-port-radar-concentric-into-secubox-commo` with title "Port radar_concentric into secubox_common + phase-aware dashboards (closes #138)"
- New `secubox_common.painters.radar_concentric` module (phase-aware); module→arc-angle decoupled from list order via `DEFAULT_NAME_TO_ANGLE`
- `RoundDashboard.layout(metrics, phase=0.0)` + `SquareDashboard.layout(metrics, phase=0.0)` — backward-compatible (phase=0 = still frame)
- Square kiosk `__main__` drives `phase = (time.monotonic() * 12.0 / 60.0) % 1.0` at 12 RPM (matches deployed `fallback_manager._sweep_speed`)
- 118 / 118 tests green (36 secubox_common incl. 8 new + 78 square kiosk + 4 round)
- **Hardware bench (Pi 4B + 7" DSI, 2026-05-15):** rotating radar ✓ · icons ✓ · right panel ✓ — user-confirmed
- `fallback_manager.py` migration deferred (visual-palette decision needed; follow-up)
---
## 🔄 2026-05-15: Round image cleanup — dead ifupdown + secubox sudo + OTG comment (Issue [#139](https://github.com/CyberMind-FR/secubox-deb/issues/139), PR [#143](https://github.com/CyberMind-FR/secubox-deb/pull/143) OPEN)
### Original misdiagnosis → rescoped
Initial report claimed "OTG networking dead": `/etc/network/interfaces.d/usb0` was a static stanza for ifupdown, but `ifupdown` was missing → `usb0` stayed DOWN. Wrong conclusion. Live-system probe via ACM serial showed the actual binding is on `usb1` (10.55.0.2/30), set programmatically by `secubox-otg-gadget.sh`. The dead ifupdown stanza never did anything; OTG was always working.
### Rescoped fix
- Drop dead `/etc/network/interfaces.d/usb0` (file + the inline heredoc in `build-eye-remote-image.sh` that recreated it)
- Add `secubox` user to `sudo` group (so ACM serial recovery is possible — previously the only-path-in had no path-to-fix)
- Rewrite the misleading `usb1 = ECM` comment in `secubox-otg-gadget.sh` — RNDIS+ECM share host_addr so host reaches 10.55.0.2 via either function
### Status
- Issue opened 2026-05-15 10:27; PR [#143](https://github.com/CyberMind-FR/secubox-deb/pull/143) opened 2 minutes later on `fix/139-round-image-usb0-otg-networking-dead-ifu`
- Initial misdiagnosis annotated as a comment on issue #139 — diagnostic confusion came from the dead stanza being visible
- **Hardware bench (Pi Zero W 1st gen, HyperPixel 2.1 Round, 2026-05-15):** all 3 fixes verified live:
- `/etc/network/interfaces.d/` directory absent ✓
- `secubox` in `sudo` group (`27(sudo)`) ✓
- Gadget composer comment rewritten ✓
- Bonus: ping `10.55.0.2` from host 3/3 received at 0.3 ms, SSH port 22 OPEN, both `usb0` + `usb1` UP @ 10.55.0.2/30 on the Pi
- Mid-bench: caught a pre-existing Pi Zero W `dwc2` kernel panic under host xHCI reset hammering — unrelated to #143 (image was good, dwc2 driver instability on ARMv6 under USB stress); deferred to a separate investigation
---
## 🔄 2026-05-15: CMSD SPDX header rollout (Issue [#81](https://github.com/CyberMind-FR/secubox-deb/issues/81))
### Status
Worktree `secubox-deb-license-wt` on branch `feature/license-phase-b-full` at `aa1f7481` ("enroll all in-scope files via `**` allowlist (Phase B + C)"). Phase A + B + C work all on the branch but no PR opened yet.
---
## 🧹 2026-05-15: worktree + local-state housekeeping
### Cleaned 2026-05-15
- `secubox-deb-worktrees/127-add-remote-ui-square-variant-for-pi-4b-7` (Phase 1, PR #130 merged as `7c37415f`) — removed
- `secubox-deb-worktrees/127-phase2-square-variant` (Phase 2, PR #131 closed/superseded) — removed
- `secubox-deb-worktrees/127-phase3-python-kiosk` (Phase 3, PR #132 merged as `dee8bf8b`) — force-removed (had two stray untracked Signal Desktop apt-key files unrelated to the project, safe to discard)
All three feature branches deleted locally. `agent-worktree.sh clean` resolves by issue number which collides for multi-worktree issues like #127; used direct `git worktree remove` + `git branch -D`.
### Local master state
- `master` synced with `origin/master` at `a313816e` (pushed `839bab94..a313816e`, 6 commits). Was 6 ahead / 1 behind earlier; rebased then pushed.
- Untracked: `.claude/settings.json` (pre-existing, intentional)
### Worktrees still active
| Worktree | Branch | Backing PR |
|---|---|---|
| `135-converge-round-square-dashboards-into-re` | `feature/135-…` | #140 OPEN |
| `136-mail-stack-phase-1-source-catch-up-legac` | `feature/136-…` | #141 OPEN |
| `138-port-radar-concentric-into-secubox-commo` | `feature/138-…` | #142 OPEN |
| `139-round-image-usb0-otg-networking-dead-ifu` | `fix/139-…` | #143 OPEN |
| `secubox-deb-license-wt` | `feature/license-phase-b-full` | none (SPDX rollout #81) |
---
@ -20,11 +132,24 @@ Replace Phase 2's Chromium+PySide6 dual-window stack with a single-process Pytho
- Phase 2 PR #131 closed (superseded by Phase 3)
- **Squash-merged 2026-05-14 as `dee8bf8b`** on master (after Phase 1 `7c37415f`)
### Followups
### Hardware gates — 3 of 4 closed (2026-05-15)
- **Task 24 (Pi 400 manual sanity) — IN PROGRESS** this session: building `secubox-eye-square_0.2.0_arm64.img.xz` locally for flash to uSD.
- **Task 23 (Pi 4B manual bench)** — still pending hardware (build + flash + boot + kiosk visible + OTG link to MOCHAbin).
- Issue #127 stays open until both Pi 4B and Pi 400 benches pass.
| Task | Hardware | Status |
|------|----------|--------|
| **Task 23** — Pi 4B square/ manual bench | Pi 4B + official 7" DSI 800×480 | ✅ kiosk renders correctly post-#134 fixes |
| **Task 24** — Pi 400 square/ sanity | Pi 400 + HDMI 1920×1080 | ✅ same image, kiosk center-padded into letterbox (PR #134 second commit) |
| **Task 19** — Pi Zero W round/ manual bench | Pi Zero W + HyperPixel 2.1 | ✅ booted from CI-built `secubox-eye-remote-2.2.1.img.xz`, rainbow ring dashboard clean post-`common/` |
| Task 18 — round/ `diffoscope` regression gate | n/a (automated) | ⏳ still blocked on `hyperpixel2r.dtbo` prerequisite |
### Bug haul from the bench (fixed in PR [#134](https://github.com/CyberMind-FR/secubox-deb/pull/134), merged `a3a918ed`)
1. `/run/secubox` not recreated at boot (tmpfs wipe) → added `tmpfiles.d/secubox-eye-square.conf`
2. `fonts-dejavu-core` missing from chroot apt-install → added
3. `draw.text()` calls relied on Pillow legacy bitmap default (no Unicode) → `theme.DEFAULT_FONT` + `font=` kwarg on all 25 call sites
4. `framebuffer.py` hardcoded 32bpp BGRA but `vc4drmfb` is 16bpp RGB565 → numpy RGB565 packer + `bits_per_pixel` auto-detect
5. (followup) `framebuffer.py` hardcoded 800×480 → `virtual_size` auto-detect + center-pad for HDMI
Issue #127 closure now gated only on Task 18. Bug #139 (round image OTG networking) and enhancement #138 (radar to common) were surfaced from the same bench session — see today's PRs above.
---

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
# Mail Phase 1 — Rollback recipe
Backups produced 2026-05-15 07:41 on test board 192.168.1.200 by
`docs/superpowers/plans/2026-05-15-mail-phase1-lxc-consolidation.md` Task 0.
## What's in `/srv/backups/mail-phase1/`
| File | Size | Contents |
|---|---|---|
| `data-volumes-mail-2026-05-15-0741.tar.gz` | 48K | Entire `/data/volumes/mail/` tree — vmail dirs for `secubox.in/{gk2,bat,bourdon,lemurien,ragondin}`, Postfix lookup tables, ACME certs |
| `lxc-mail-config-2026-05-15-0741.tar.gz` | 4.0K | `/data/lxc/mail/config` (the LXC's unprivileged-veth network config) |
| `mail-toml-2026-05-15-0741.bak` | 0.4K | Original `/etc/secubox/mail.toml` (still has legacy keys) |
| `pkglist-2026-05-15-0741.txt` | 0.7K | `dpkg -l` output for `secubox-mail*` + `secubox-webmail*` pre-deploy |
## Rollback procedure
If Phase 1 deploy breaks the mail stack on the board:
```bash
ssh root@192.168.1.200 'set -euo pipefail
lxc-stop -n mail 2>/dev/null || true
# Restore /data/volumes/mail (vmail + config + ssl)
rm -rf /data/volumes/mail
tar -xzf /srv/backups/mail-phase1/data-volumes-mail-2026-05-15-0741.tar.gz -C /
# Restore LXC config
tar -xzf /srv/backups/mail-phase1/lxc-mail-config-2026-05-15-0741.tar.gz -C /
# Restore toml + downgrade packages
cp /srv/backups/mail-phase1/mail-toml-2026-05-15-0741.bak /etc/secubox/mail.toml
apt install --allow-downgrades -y \
secubox-mail=2.1.0-1~bookworm1 \
secubox-mail-lxc=1.1.0-1~bookworm1 \
secubox-webmail=1.0.0-1~bookworm1 \
secubox-webmail-lxc=1.1.0-1~bookworm1
systemctl restart secubox-mail nginx haproxy'
```
## Priority guarantees
- The data tarball preserves the 5 live `secubox.in` mailboxes (`gk2`, `bat`,
`bourdon`, `lemurien`, `ragondin`) and the ACME certs from Feb 2026.
- Per spec rev. 2 invariant **I13**, this data MUST NOT be lost. If anything
goes wrong, restoring this tarball is the first and most important step.

View File

@ -0,0 +1,316 @@
# Mail Stack Architecture — Phase 0 Design (rev. 2)
**Date:** 2026-05-15 (rev. 2 — reconciled with board reality)
**Status:** Approved direction; revised invariants pending user re-confirmation
**Author:** Gérald Kerma <devel@cybermind.fr>
**Scope:** Architecture-only. Each implementation phase below gets its own spec → plan → PR cycle.
> **Revision note (rev. 2, 2026-05-15):** Initial draft assumed a `/srv/lxc/` + `192.168.255.x` + `lxc.net.0.type = none` greenfield layout. Live board inspection on 2026-05-15 showed the actual single-`mail` LXC has already been hand-built on the test board with a modern unprivileged-veth layout under `/data/lxc/` + `/data/volumes/`. Invariants have been corrected; Phase 1 is reduced from "consolidate two LXCs" to "catch the repo source up to where the board already is, then deprecate the legacy package frame".
---
## 1. Goal
Deliver a "full-featured" multi-domain mail + collaboration stack inside a single LXC container, integrated with the existing SecuBox-Deb identity (`secubox-users`), DNS (`secubox-dns`) and storage (`secubox-nextcloud`) services.
**Definition of full-featured for this project:**
- Standards-compliant SMTP/IMAP with TLS, SPF, DKIM, DMARC, ARC
- Per-user features: ManageSieve filters, quotas, vacation, app passwords
- Multi-domain virtual hosting with per-domain DKIM keys
- Roundcube webmail with PGP (Enigma), 2FA, ManageSieve UI, CardDAV/CalDAV bridged to Nextcloud
- Mailing lists (mlmmj) and shared mailboxes
- imapsync-based migration from OpenWrt SecuBox or any external IMAP
- End-user self-service portal (password, vacation, aliases, app-passwords, sieve)
- Observability + outbound abuse policies
## 2. Non-goals
- ActiveSync / EAS protocol (Z-Push, grommunio) — not in scope this round
- JMAP (Cyrus pivot) — not in scope
- Mail archival / legal hold — not in scope
- Independent CardDAV/CalDAV server inside the mail LXC — delegated to `secubox-nextcloud`
## 3. Locked invariants
Each phase below MUST respect these. Changing one requires a new Phase 0 revision.
| # | Invariant |
|---|---|
| **I1** | Exactly ONE LXC named `mail` at `/data/lxc/mail` (symlinked from `/var/lib/lxc/mail`). No separate `mailserver` or `roundcube` LXCs. |
| **I2** | LXC is **unprivileged** (`lxc.idmap = u 0 100000 65536`), veth on bridge `br-lxc`, IPv4 `10.100.0.10/24` with gateway `10.100.0.1`. AppArmor + `debian.common.conf` includes. |
| **I3** | Antispam stack is **Rspamd** (single daemon: greylisting + spam scoring + DKIM sign+verify + SPF + DMARC + ARC). ClamAV remains as separate AV milter. SpamAssassin, Postgrey, OpenDKIM, opendmarc removed in Phase 2. |
| **I4** | CardDAV + CalDAV are **not** served from the mail LXC. Roundcube plugins point at `https://nextcloud.gk2.secubox.in/remote.php/dav/`. |
| **I5** | Mail accounts are provisioned **by** `secubox-users`. The mail stack is a downstream consumer. Local Dovecot is the materialized projection. |
| **I6** | Outbound delivery is **direct on port 25**. No smarthost relay. |
| **I7** | Multi-domain virtual users. Mailbox path: `/data/volumes/mail/vmail/<domain>/<user>/Maildir/`. Per-domain DKIM key (or Rspamd selector after Phase 2). |
| **I8** | Existing data is migrated from OpenWrt SecuBox via **imapsync** (Phase 7). |
| **I9** | Webmail = Roundcube. SOGo / Cyrus / grommunio rejected. |
| **I10** | All container daemons listen on the LXC IP (`10.100.0.10`). Exposed to LAN/WAN via host's HAProxy (SMTP/IMAPS TCP pass-through) + nginx (admin + webmail HTTPS). |
| **I11** | Configuration source of truth: `/etc/secubox/mail.toml` on the host. Rendered into the LXC by `mailctl`. No editing config inside the LXC. |
| **I12** | **Persistent data lives on the host under `/data/volumes/mail/{vmail,config,ssl}`** and is bind-mounted into the LXC. Destroying the LXC rootfs MUST be safe — no production data lives in the rootfs. |
| **I13** | **Existing mail data on the test board MUST be preserved.** As of 2026-05-15 the board hosts the `secubox.in` domain with five live mailboxes (`gk2`, `bat`, `bourdon`, `lemurien`, `ragondin`) under `/data/volumes/mail/vmail/secubox.in/`. Any upgrade path that touches the data directory MUST refuse to proceed if it cannot guarantee preservation. |
## 4. Current state (test board 192.168.1.200, surveyed 2026-05-15)
| Element | Reality |
|---|---|
| LXCs on board | `gitea`, `mail`, `matrix`, `mitmproxy`, `nextcloud`, `streamlit` |
| `mail` LXC location | `/data/lxc/mail/` (symlinked from `/var/lib/lxc/mail`) |
| `mail` LXC state | STOPPED (last touched 2026-05-08) |
| `mail` LXC networking | unprivileged, veth `br-lxc`, `10.100.0.10/24`, gw `10.100.0.1` |
| `mail` LXC bind mounts | `/data/volumes/mail/vmail``var/vmail`, `/data/volumes/mail/config``etc/mail-config`, `/data/volumes/mail/ssl``etc/ssl/mail` |
| Inside-LXC software | Postfix, Dovecot (core+imapd+lmtpd+pop3d), Apache2+mod_php, nginx, OpenDKIM, SpamAssassin, Roundcube (core+plugins+classic+larry skins, mysql backend), php-net-sieve |
| **NOT yet inside LXC** | Postgrey, ClamAV (planned by spec rev. 1 — never installed; rev. 2 drops Postgrey entirely and defers ClamAV to Phase 2) |
| Persistent data | `/data/volumes/mail/vmail/{secubox.in/{gk2,bat,bourdon,lemurien,ragondin},gk2}`, `/data/volumes/mail/config/{main.cf,master.cf,vmailbox,virtual,vdomains,users,aliases,...}`, `/data/volumes/mail/ssl/{fullchain.pem,privkey.pem}` (Feb 2026 ACME issue) |
| Host packages | `secubox-mail 2.1.0-1`, `secubox-mail-lxc 1.1.0-1`, `secubox-webmail 1.0.0-1`, `secubox-webmail-lxc 1.1.0-1` |
| Host service | `secubox-mail.service` is `active` (FastAPI listens, but mail LXC isn't running) |
| Postfix `main.cf` (in `/data/volumes/mail/config/`) | hostname `mail.secubox.in`, virtual mailbox domains via `/etc/postfix/vdomains`, SASL via Dovecot, TLS via `/etc/ssl/mail/`, Maildir layout |
| Roundcube webserver | Apache2 + libapache2-mod-php8.2 (BOTH nginx and apache2 packages installed inside LXC; only one needed) |
| Repo source layout (this tree) | Out of date: `mailctl` still references `/srv/lxc`, `/srv/mail`, `mail_container = "mailserver"`, `webmail_container = "roundcube"`, `192.168.255.30`. The single `mail` LXC was hand-built outside the repo. |
| Host `mail.toml` | Out of date: still has `mail_container`, `webmail_container`, `mail_ip = "192.168.255.30"`, `webmail_ip = "192.168.255.31"` |
## 5. Target architecture
### 5.1 LXC layout (canonical)
```
/var/lib/lxc/mail -> /data/lxc/mail (symlink, host-side)
/data/lxc/mail/
config # LXC config (unprivileged, veth, br-lxc, 10.100.0.10/24)
rootfs/ # Debian bookworm arm64
etc/postfix/ # rendered by mailctl (read-only at runtime)
etc/dovecot/
etc/rspamd/ # Phase 2+
etc/clamav/ # Phase 2+
etc/apache2/ # Phase 1 keeps Apache; Phase 5 may revisit
etc/roundcube/
etc/mlmmj/ # Phase 6+
opt/start-mail.sh # init script run by lxc.init.cmd
/data/volumes/mail/ # Persistent data (bind-mounted into LXC)
vmail/ # Maildirs
secubox.in/<user>/Maildir/
<future-domain>/<user>/Maildir/
config/ # Postfix/Dovecot lookup tables, owned by host
main.cf, master.cf
users, vmailbox, virtual, valias, vdomains, aliases
*.lmdb (rebuilt by postmap)
ssl/ # ACME-issued certs (host renews, container reads)
fullchain.pem, privkey.pem
dkim/ # per-domain keys (Phase 2 owned by Rspamd)
rspamd/ # Phase 2 — bayes corpus, history
clamav/ # Phase 2 — virus signature DB
sieve/ # Phase 4 — per-user sieve scripts
mlmmj/ # Phase 6 — mailing list spools
roundcube/ # Phase 5 — user data, logs, plugin state
```
### 5.2 Network and ports
LXC IP: `10.100.0.10` (br-lxc). Gateway: `10.100.0.1` (host bridge).
| Listener | Port | Protocol | Exposed how |
|---|---|---|---|
| Postfix smtpd | 25 | SMTP | HAProxy TCP pass-through, WAN |
| Postfix submission | 587 | SMTP+STARTTLS+SASL | HAProxy TCP pass-through, WAN |
| Postfix submissions | 465 | SMTPS+SASL | HAProxy TCP pass-through, WAN |
| Dovecot imap | 143 | IMAP+STARTTLS | LAN only |
| Dovecot imaps | 993 | IMAPS | HAProxy TCP pass-through, WAN |
| Dovecot ManageSieve | 4190 | sieve+STARTTLS | HAProxy TCP pass-through, WAN |
| Rspamd controller | 11334 | HTTP | Behind host nginx admin auth (Phase 2+) |
| Rspamd worker | 11332 | milter | Localhost-in-LXC only (Phase 2+) |
| ClamAV milter | 8894 | milter | Localhost-in-LXC only (Phase 2+) |
| Roundcube HTTP (Apache or nginx) | 80 / 443 | HTTP | Behind host nginx on `webmail.<domain>` |
Host nginx publishes:
- `https://mail-admin.gk2.secubox.in/` → FastAPI on UNIX socket `/run/secubox/mail.sock`
- `https://webmail.gk2.secubox.in/``http://10.100.0.10:80/` (Roundcube)
- `https://mail.gk2.secubox.in/.well-known/autoconfig/...` → FastAPI autoconfig
- `https://rspamd.gk2.secubox.in/``http://10.100.0.10:11334/` (Phase 2+, admin-auth gated)
### 5.3 Daemon inventory (end of Phase 8)
Inside `mail` LXC:
| Daemon | Source | Role | Phase added |
|---|---|---|---|
| Postfix | Debian | MTA | already on board |
| Dovecot | Debian | IMAP + LMTP + ManageSieve + SASL auth | already on board |
| Apache2 + mod_php | Debian | Roundcube webserver | already on board (Phase 5 may migrate to nginx+php-fpm) |
| Roundcube | Debian | Webmail (with classic/larry skins, plugins) | already on board |
| Rspamd | Debian | Greylist + spam + DKIM + SPF + DMARC + ARC + ratelimit | Phase 2 |
| ClamAV (clamd + clamav-milter) | Debian | Virus scan | Phase 2 |
| mlmmj | Debian | Mailing lists | Phase 6 |
| acme.sh | upstream | TLS cert renewal | host-side, already wired |
| imapsync | upstream | One-shot per migration job | Phase 7 |
**Daemons removed by Phase 2:** SpamAssassin, OpenDKIM. (Postgrey was planned by rev. 1 but never installed; dropped from scope.)
### 5.4 Identity / provisioning flow (Phase 3)
```
secubox-users API ──"user.created"──▶ mail provisioning webhook
mailctl provision <user@domain>
├──▶ /data/volumes/mail/vmail/<domain>/<user>/Maildir (mkdir + perms)
├──▶ append /data/volumes/mail/config/users (Dovecot passwd-file, SHA512-CRYPT)
├──▶ append /data/volumes/mail/config/vmailbox (Postfix virtual_mailbox_maps)
└──▶ postmap if needed; notify Rspamd
```
Password sync: `secubox-users` POSTs `/internal/password` over UNIX socket on every change. No password ever leaves the host except as the SHA512-CRYPT hash already stored in Dovecot's `users` file.
### 5.5 DNS records owned by the mail stack
For each managed domain, `mailctl dns-records <domain>` emits records `secubox-dns` must publish:
```
mail.<domain> A <public IP>
<domain> MX 10 mail.<domain>.
<domain> TXT "v=spf1 mx -all"
default._domainkey.<domain> TXT "v=DKIM1; k=rsa; p=<pubkey>"
_dmarc.<domain> TXT "v=DMARC1; p=quarantine; rua=mailto:postmaster@<domain>; ruf=mailto:postmaster@<domain>; adkim=s; aspf=s"
_imaps._tcp.<domain> SRV "0 1 993 mail.<domain>."
_submission._tcp.<domain> SRV "0 1 587 mail.<domain>."
autoconfig.<domain> CNAME mail.<domain>.
autodiscover.<domain> CNAME mail.<domain>.
```
Phase 3 wires this to `secubox-dns` via API.
### 5.6 `mail.toml` schema (target — end of Phase 3)
```toml
[mail]
enabled = true
hostname = "mail.gk2.secubox.in"
container = "mail"
lxc_path = "/var/lib/lxc" # symlink to /data/lxc on this board
data_path = "/data/volumes/mail"
lxc_ip = "10.100.0.10"
lxc_bridge = "br-lxc"
lxc_gateway = "10.100.0.1"
[[mail.domain]]
name = "secubox.in"
primary = true
dkim_selector = "default"
dmarc_policy = "quarantine"
catchall = ""
[mail.tls]
provider = "acme"
acme_email = "postmaster@secubox.in"
[mail.rspamd] # Phase 2+
greylist = true
bayes_autolearn = true
ratelimit_outbound = "100/h/user"
[mail.identity] # Phase 3+
source = "secubox-users"
provisioning_url = "http://127.0.0.1:8093/api/v1/users"
[mail.dav] # Phase 5+
provider = "secubox-nextcloud"
url = "https://nextcloud.gk2.secubox.in/remote.php/dav/"
[mail.webmail] # Phase 5+
enabled = true
url = "https://webmail.gk2.secubox.in/"
plugins = ["managesieve", "carddav", "calendar", "enigma", "twofactor"]
[mail.lists] # Phase 6+
enabled = false
default_domain = "lists.gk2.secubox.in"
```
## 6. Phase plan (revised)
Phase 1 is now substantially smaller: most of the architectural bones are already on the board; the repo source just doesn't reflect them yet.
| # | Phase | Effort | Critical-path? |
|---|---|---|---|
| **0** | Architecture spec (this doc, rev. 2) | done | — |
| **1** | **Reconcile source ↔ board, deprecate legacy packages, lock the data contract** | 23 days | yes |
| **2** | Rspamd migration (drops SA + OpenDKIM, adds ClamAV) | 1 wk | yes |
| **3** | Multi-domain + `secubox-users` provisioning hook | 1.5 wk | yes |
| **4** | ManageSieve + quotas + vacation | 1 wk | yes |
| **5** | Roundcube polish + Nextcloud DAV bridge + (optional) Apache→nginx+php-fpm | 1 wk | no |
| **6** | mlmmj mailing lists + shared mailboxes | 1 wk | no |
| **7** | imapsync migration tooling | 1 wk | no |
| **8** | Self-service portal + observability + outbound abuse policies | 1.5 wk | no |
**Total:** ~8 weeks. Phase 58 can interleave once Phase 3 is in.
### Phase 1 — revised goal: "source-catch-up + legacy package cleanup"
**Deliverables**
- Repo source updated to canonical paths/IP: `/var/lib/lxc/mail`, `/data/volumes/mail`, `10.100.0.10`, unprivileged veth br-lxc. (`mailctl`, `mailserverctl`, `roundcubectl`, `api/main.py`.)
- `mail.toml` schema: single `container`, `lxc_ip`, `lxc_bridge`, `lxc_gateway`, `data_path`. Drop `mail_container`/`webmail_container`/`mail_ip`/`webmail_ip`/`webmail_port`.
- `lib/install.sh` + `lib/lxc.sh` extracted from `mailserverctl` for re-use.
- `mailctl migrate-config` rewrites a legacy `mail.toml` in place. Idempotent.
- `mail-migrate-to-single-lxc.sh` becomes a defensive **scanner** that detects old `mailserver`/`roundcube` LXC directories (none expected on this board) and old toml keys, and applies safe migration. **Refuses to touch `/data/volumes/mail/` if data is present** (per I13).
- Legacy `secubox-mail-lxc`, `secubox-webmail-lxc`, `secubox-webmail` packages → transitional metadata-only `2.2.0` packages that just `Depends: secubox-mail (>= 2.2)`.
- `secubox-mail` bumps to `2.2.0` (one minor higher than current `2.1.0`) with `Breaks:`/`Replaces:` against the transitional packages.
- Host nginx vhost: `mail-admin.<base>` → FastAPI socket; `webmail.<base>``http://10.100.0.10:80/`. Replaces both `packages/secubox-mail/nginx/mail.conf` and `packages/secubox-webmail/nginx/webmail.conf` with one `common/nginx/modules.d/mail.conf`.
- HAProxy SMTP/submission/IMAPS/sieve backends targeting `10.100.0.10`.
- API `main.py` updated to read new keys; all 62 endpoints respond non-5xx (presence test).
- Acceptance: from clean checkout + deploy, `mailctl status` correctly reports the existing `mail` LXC; `mailctl start` brings it up; existing 5 `secubox.in` users can IMAP login; Roundcube responds via host proxy.
**Explicitly out of Phase 1:**
- Installing Postgrey / ClamAV inside the LXC — Phase 2 handles ClamAV; Postgrey is dropped entirely.
- Multi-domain refactor — Phase 3.
- Apache → nginx+php-fpm migration — Phase 5 if desired.
- Roundcube CardDAV/CalDAV plugin wiring — Phase 5.
## 7. Deprecations and breaking changes
| Item | Phase | Migration |
|---|---|---|
| `secubox-mail-lxc` package | 1 | Transitional 2.2.0 stub depending on `secubox-mail (>= 2.2)`. Removed entirely in 3.0. |
| `secubox-webmail-lxc` package | 1 | Same |
| `secubox-webmail` package | 1 | Same — its API surface folded into `secubox-mail` API. |
| `mail_container`, `webmail_container`, `mail_ip`, `webmail_ip`, `webmail_port` in `mail.toml` | 1 | `mailctl migrate-config` rewrites to single `container`/`lxc_ip` + comments the old keys for one release. |
| `/srv/lxc/`, `/srv/mail/` paths in source | 1 | Replaced by `/var/lib/lxc/` and `/data/volumes/mail/` everywhere. |
| `192.168.255.30/31` IP literals in source | 1 | Replaced by `10.100.0.10` (and `lxc_ip` lookup from toml). |
| OpenDKIM (`/dkim/*` API) | 2 | Rspamd DKIM module; old endpoints proxy for one minor version, removed in 3.0. |
| SpamAssassin (`/spam/*`) | 2 | Rspamd spam scoring; same pattern. |
| Postgrey (`/grey/*`) | 2 | Rspamd greylist module; the `/grey/*` endpoints were stubbed but Postgrey was never installed — endpoints return informative deprecation responses. |
| `domain` scalar in `mail.toml` | 3 | Migrated to `[[mail.domain]]` array. |
## 8. GitHub issue plan
| # | Title | Label | Phase |
|---|---|---|---|
| TBD | Mail stack: Phase 1 — source-catch-up + legacy package cleanup | `migration,wip` | 1 |
| TBD | Mail stack: Phase 2 — Rspamd migration | `migration,security` | 2 |
| TBD | Mail stack: Phase 3 — multi-domain + secubox-users integration | `migration,api` | 3 |
| TBD | Mail stack: Phase 4 — ManageSieve + quotas + vacation | `api,frontend` | 4 |
| TBD | Mail stack: Phase 5 — Roundcube polish + Nextcloud DAV bridge | `frontend` | 5 |
| TBD | Mail stack: Phase 6 — mailing lists + shared mailboxes | `api,frontend` | 6 |
| TBD | Mail stack: Phase 7 — imapsync migration tooling | `migration` | 7 |
| TBD | Mail stack: Phase 8 — self-service portal + metrics + abuse | `frontend,infra` | 8 |
Issues filed at start of each phase.
## 9. Open questions (deferred to per-phase specs)
- **Roundcube webserver (Phase 5):** Keep Apache+mod_php (current) or migrate to nginx+php-fpm? Decided in Phase 5 spec. Phase 1 does **not** touch this.
- **PGP key escrow (Phase 5/8):** read-only after import vs. user-managed in self-service portal?
- **HAProxy SMTP cert handling:** TCP pass-through (current direction) vs. terminate at HAProxy with shared cert. Phase 1 stays pass-through; revisit only if cert renewal proves painful.
- **Mailing list tool (Phase 6):** mlmmj vs. Mailman 3?
## 10. ANSSI / CSPN posture
- **Privilege separation:** every daemon under its own user. LXC unprivileged adds a second layer (root inside LXC = uid 100000 outside).
- **Audit logging:** all admin actions (provision, delete, password reset, sieve edit) appended to `/data/volumes/mail/audit.log` and to `secubox-users` audit stream.
- **Double-buffer config:** `mailctl` writes Postfix/Dovecot/Rspamd config under `/data/volumes/mail/config/shadow/`, validates with `postfix check` / `doveconf -n`, atomic-swap to `active/`. Keeps R1..R4.
- **AppArmor profiles:** one per daemon, shipped by `secubox-mail` debian/, enforced via `postinst`.
- **Secrets:** Dovecot SHA512-CRYPT only; DKIM private keys 0600 owned by `_rspamd` (post-Phase-2); ACME private keys 0600 owned by root. Nothing leaves the host.
---
**End of Phase 0 spec rev. 2.** Next: revised Phase 1 plan, then user re-confirmation, then execution.

View File

@ -1,3 +1,10 @@
secubox-mail-lxc (2.2.0-1~bookworm1) bookworm; urgency=medium
* Transitional package — all functionality moved to secubox-mail >= 2.2.
* Closes: #136
-- Gerald KERMA <devel@cybermind.fr> Fri, 15 May 2026 12:00:00 +0200
secubox-mail-lxc (1.1.0-1~bookworm1) bookworm; urgency=medium
* Remove standalone menu entry (now integrated into secubox-mail UI)

View File

@ -1,5 +1,5 @@
Source: secubox-mail-lxc
Section: admin
Section: oldlibs
Priority: optional
Maintainer: Gerald KERMA <devel@cybermind.fr>
Build-Depends: debhelper-compat (= 13)
@ -7,15 +7,8 @@ Standards-Version: 4.6.2
Package: secubox-mail-lxc
Architecture: all
Depends: ${misc:Depends}, secubox-core (>= 1.0.0), lxc, wget
Recommends: secubox-mail
Description: SecuBox Mail LXC Container (Backend)
LXC container management for Postfix/Dovecot mail server.
Backend component consumed by secubox-mail (no standalone UI).
.
Provides Alpine Linux container with:
- Postfix MTA
- Dovecot IMAP/POP3
- OpenDKIM signing
.
Install secubox-mail for the management UI.
Depends: ${misc:Depends}, secubox-mail (>= 2.2)
Description: Transitional package — mail LXC functionality moved to secubox-mail
The single consolidated mail LXC is now installed and driven by
secubox-mail (>= 2.2). This package ships no files. Safe to
apt-get autoremove after upgrade.

View File

@ -1,9 +1,18 @@
#!/bin/sh
set -e
# Transitional package (secubox-mail-lxc 2.2.0) — clean up the old
# standalone service if it's still around from <2.2 installs.
if [ "$1" = "configure" ]; then
systemctl daemon-reload
systemctl enable secubox-mail-lxc.service || true
systemctl start secubox-mail-lxc.service || true
if [ -e /lib/systemd/system/secubox-mail-lxc.service ] \
|| [ -e /etc/systemd/system/secubox-mail-lxc.service ]; then
systemctl stop secubox-mail-lxc.service 2>/dev/null || true
systemctl disable secubox-mail-lxc.service 2>/dev/null || true
rm -f /lib/systemd/system/secubox-mail-lxc.service \
/etc/systemd/system/secubox-mail-lxc.service
systemctl daemon-reload || true
fi
fi
#DEBHELPER#
exit 0

View File

@ -1,9 +1,5 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
systemctl stop secubox-mail-lxc.service || true
systemctl disable secubox-mail-lxc.service || true
systemctl reload nginx 2>/dev/null || true
fi
# Transitional package — nothing to undo.
#DEBHELPER#
exit 0

View File

@ -1,14 +1,12 @@
#!/usr/bin/make -f
# Transitional package — ships no files. dh handles the metadata only.
%:
dh $@
override_dh_auto_install:
# API (used by secubox-mail for container management)
install -d debian/secubox-mail-lxc/usr/lib/secubox/mail-lxc
cp -r api debian/secubox-mail-lxc/usr/lib/secubox/mail-lxc/
# Control scripts
install -d debian/secubox-mail-lxc/usr/sbin
[ -d sbin ] && install -m 755 sbin/* debian/secubox-mail-lxc/usr/sbin/ || true
# Modular nginx config (API proxy only, no separate menu)
install -d debian/secubox-mail-lxc/etc/nginx/secubox.d
[ -f nginx/mail-lxc.conf ] && cp nginx/mail-lxc.conf debian/secubox-mail-lxc/etc/nginx/secubox.d/ || true
# intentionally empty — no payload in transitional package
:
override_dh_installsystemd:
# do not install/enable the old secubox-mail-lxc.service
:

View File

@ -1,6 +1,3 @@
# /etc/nginx/secubox.d/mail-lxc.conf
# Installed by secubox-mail-lxc package
location /api/v1/mail-lxc/ {
proxy_pass http://unix:/run/secubox/mail-lxc.sock:/;
include /etc/nginx/snippets/secubox-proxy.conf;
}
# DEPRECATED in secubox-mail-lxc 2.2 — mail LXC management folded into
# secubox-mail's /api/v1/mail/. This snippet is kept empty for one release.

View File

@ -19,17 +19,27 @@ from pydantic import BaseModel
from secubox_core.auth import require_jwt
from secubox_core.config import get_config
app = FastAPI(title="SecuBox Mail", version="1.8.0")
app = FastAPI(title="SecuBox Mail", version="2.2.0")
config = get_config("mail")
DATA_PATH = Path(config.get("data_path", "/srv/mail"))
LXC_PATH = Path(config.get("lxc_path", "/srv/lxc"))
MAIL_CONTAINER = config.get("mail_container", "mailserver")
WEBMAIL_CONTAINER = config.get("webmail_container", "roundcube")
# Canonical config keys (Phase 1 rev. 2). Legacy keys are accepted as
# fallback for one release so deploys mid-upgrade don't break.
DATA_PATH = Path(config.get("data_path", "/data/volumes/mail"))
LXC_PATH = Path(config.get("lxc_path", "/var/lib/lxc"))
CONTAINER = config.get("container", config.get("mail_container", "mail"))
# Legacy alias retained for callers that still reference WEBMAIL_CONTAINER —
# webmail is now in the same single LXC.
MAIL_CONTAINER = CONTAINER
WEBMAIL_CONTAINER = CONTAINER
DOMAIN = config.get("domain", "secubox.local")
HOSTNAME = config.get("hostname", "mail")
MAIL_IP = config.get("mail_ip", "192.168.255.30")
WEBMAIL_PORT = config.get("webmail_port", 8027)
LXC_IP = config.get("lxc_ip", config.get("mail_ip", "10.100.0.10"))
LXC_BRIDGE = config.get("lxc_bridge", "br-lxc")
LXC_GATEWAY = config.get("lxc_gateway", "10.100.0.1")
# Webmail is on standard HTTP inside the LXC; host nginx proxies via :443.
WEBMAIL_PORT = 80
# Back-compat alias for any caller still reading MAIL_IP.
MAIL_IP = LXC_IP
def run_cmd(cmd: list, timeout: int = 30) -> tuple:
@ -901,21 +911,31 @@ async def fix_ports():
class SettingsUpdate(BaseModel):
domain: Optional[str] = None
hostname: Optional[str] = None
# Canonical fields (Phase 1 rev. 2)
lxc_ip: Optional[str] = None
# Legacy aliases — accepted for back-compat, mapped to lxc_ip server-side.
mail_ip: Optional[str] = None
webmail_port: Optional[int] = None
@app.get("/settings", dependencies=[Depends(require_jwt)])
async def get_settings():
"""Get mail configuration settings"""
"""Get mail configuration settings."""
return {
"domain": DOMAIN,
"hostname": HOSTNAME,
"mail_ip": MAIL_IP,
"webmail_port": WEBMAIL_PORT,
"mail_container": MAIL_CONTAINER,
"webmail_container": WEBMAIL_CONTAINER,
# Canonical
"container": CONTAINER,
"lxc_ip": LXC_IP,
"lxc_bridge": LXC_BRIDGE,
"lxc_gateway": LXC_GATEWAY,
"data_path": str(DATA_PATH),
"lxc_path": str(LXC_PATH),
# Legacy aliases (kept until v3.0)
"mail_ip": LXC_IP,
"webmail_port": WEBMAIL_PORT,
"mail_container": CONTAINER,
"webmail_container": CONTAINER,
}
@ -932,14 +952,19 @@ async def update_settings(settings: SettingsUpdate):
key, val = line.split("=", 1)
current[key.strip()] = val.strip().strip('"')
# Update values
# Update values. Canonical keys take precedence; legacy fields are
# accepted for one release and mapped to canonical.
if settings.domain:
current["domain"] = settings.domain
if settings.hostname:
current["hostname"] = settings.hostname
if settings.mail_ip:
current["mail_ip"] = settings.mail_ip
if settings.lxc_ip:
current["lxc_ip"] = settings.lxc_ip
elif settings.mail_ip:
current["lxc_ip"] = settings.mail_ip
if settings.webmail_port:
# webmail_port is no longer meaningful (Roundcube is on :80 inside LXC,
# proxied via host nginx :443). Recorded for audit only.
current["webmail_port"] = str(settings.webmail_port)
# Write config

View File

@ -0,0 +1,9 @@
"""pytest conftest — make secubox_core importable when running locally
out of the source tree (no system-wide install)."""
import pathlib
import sys
REPO_ROOT = pathlib.Path(__file__).resolve().parents[4]
COMMON = REPO_ROOT / "common"
if COMMON.is_dir() and str(COMMON) not in sys.path:
sys.path.insert(0, str(COMMON))

View File

@ -0,0 +1,93 @@
"""Phase 1 rev. 2 acceptance: every existing API endpoint still responds non-5xx
after the source-catch-up renames.
We don't care about response *content* here — only that the route is
registered and the handler doesn't 500 on a default invocation. JWT-protected
endpoints return 401 without a token; that still counts as "registered".
Phase 2+ tightens this to assert specific shapes.
"""
import pathlib
import sys
import pytest
from fastapi.testclient import TestClient
sys.path.insert(0, str(pathlib.Path(__file__).parents[2]))
from api.main import app # noqa: E402
client = TestClient(app)
# Pulled from packages/secubox-mail/api/main.py via grep '@app\.' on 2026-05-15.
# 62 endpoints — keep this list in sync if main.py adds/removes routes.
LEGACY_ROUTES = [
("GET", "/status"),
("GET", "/health"),
("GET", "/components"),
("GET", "/access"),
("GET", "/mail/config-v1.1.xml"),
("GET", "/autoconfig/mail/config-v1.1.xml"),
("GET", "/autodiscover/autodiscover.xml"),
("POST", "/autodiscover/autodiscover.xml"),
("POST", "/Autodiscover/Autodiscover.xml"),
("GET", "/.well-known/autoconfig/mail/config-v1.1.xml"),
("GET", "/users"),
("POST", "/user"),
("DELETE", "/user/foo@example.com"),
("POST", "/user/password"),
("GET", "/aliases"),
("POST", "/alias"),
("DELETE", "/alias/foo@example.com"),
("POST", "/start"),
("POST", "/stop"),
("POST", "/restart"),
("POST", "/install"),
("GET", "/webmail/status"),
("POST", "/webmail/start"),
("POST", "/webmail/stop"),
("POST", "/webmail/restart"),
("POST", "/webmail/install"),
("POST", "/migrate"),
("GET", "/backups"),
("POST", "/backup"),
("POST", "/restore/test"),
("GET", "/logs"),
("GET", "/ssl"),
("POST", "/ssl/setup"),
("GET", "/acme/status"),
("POST", "/acme/issue"),
("POST", "/acme/renew"),
("POST", "/acme/install"),
("GET", "/dns-setup"),
("POST", "/user/repair/foo@example.com"),
("POST", "/fix-ports"),
("GET", "/settings"),
("POST", "/settings"),
("GET", "/dkim/status"),
("POST", "/dkim/setup"),
("POST", "/dkim/keygen"),
("POST", "/dkim/sync"),
("GET", "/dkim/record"),
("GET", "/spam/status"),
("POST", "/spam/setup"),
("POST", "/spam/enable"),
("POST", "/spam/disable"),
("POST", "/spam/update"),
("GET", "/grey/status"),
("POST", "/grey/setup"),
("POST", "/grey/enable"),
("POST", "/grey/disable"),
("GET", "/av/status"),
("POST", "/av/setup"),
("POST", "/av/enable"),
("POST", "/av/disable"),
("POST", "/av/update"),
("GET", "/example.com.mobileconfig"),
]
@pytest.mark.parametrize("method,path", LEGACY_ROUTES)
def test_route_responds(method, path):
resp = client.request(method, path, json={})
assert resp.status_code < 500, (
f"{method} {path}{resp.status_code}: {resp.text[:200]}"
)

View File

@ -1,21 +1,21 @@
# SecuBox Mail Server Configuration
# SecuBox Mail Server Configuration — Phase 1 rev. 2 (single LXC, canonical paths)
[mail]
enabled = true
domain = "secubox.local"
hostname = "mail"
data_path = "/srv/mail"
lxc_path = "/srv/lxc"
# Mail server container
mail_container = "mailserver"
mail_ip = "192.168.255.30"
# Single consolidated LXC (Phase 1 rev. 2)
container = "mail"
lxc_ip = "10.100.0.10"
lxc_bridge = "br-lxc"
lxc_gateway = "10.100.0.1"
lxc_path = "/var/lib/lxc"
data_path = "/data/volumes/mail"
# Webmail container
webmail_container = "roundcube"
webmail_ip = "192.168.255.31"
webmail_port = 8027
# Webmail is served by the same LXC; this URL is the host-side proxy target
webmail_url = "https://webmail.gk2.secubox.in"
# SSL settings
ssl_provider = "acme" # acme, manual, none
ssl_provider = "acme" # acme | manual | none
acme_email = ""

View File

@ -1,3 +1,20 @@
secubox-mail (2.2.0-1~bookworm1) bookworm; urgency=medium
* Phase 1 source-catch-up: canonical paths /var/lib/lxc/mail +
/data/volumes/mail, IP 10.100.0.10 (unprivileged veth br-lxc).
* Extract lib/install.sh + lib/lxc.sh + lib/migrate.sh from mailserverctl.
* mailctl gains migrate-config subcommand; mailserverctl + roundcubectl
reduced to deprecation shims (will be removed in 3.0).
* mail-migrate-to-single-lxc.sh added as defensive scanner — respects
spec invariant I13 (existing /data/volumes/mail/vmail data preserved).
* HAProxy mail-TCP snippet shipped at packages/secubox-mail/haproxy/
targeting 10.100.0.10.
* Breaks/Replaces transitional secubox-mail-lxc / secubox-webmail-lxc /
secubox-webmail packages (<< 2.2).
* Closes: #136
-- Gerald KERMA <devel@cybermind.fr> Fri, 15 May 2026 12:00:00 +0200
secubox-mail (2.1.0-1~bookworm1) bookworm; urgency=medium
* Enhanced frontend with security features dashboard

View File

@ -8,6 +8,8 @@ Standards-Version: 4.6.2
Package: secubox-mail
Architecture: all
Depends: ${misc:Depends}, secubox-core (>= 1.0.0), lxc, debootstrap, openssl
Breaks: secubox-mail-lxc (<< 2.2), secubox-webmail (<< 2.2), secubox-webmail-lxc (<< 2.2)
Replaces: secubox-mail-lxc (<< 2.2), secubox-webmail (<< 2.2), secubox-webmail-lxc (<< 2.2)
Suggests: acme.sh
Description: SecuBox Mail Module
Complete email server management API and dashboard.

View File

@ -1,9 +1,23 @@
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
# Phase 1 rev. 2: on upgrade from < 2.2, rewrite legacy toml + run
# the defensive scanner. Both are idempotent and refuse to touch
# /data/volumes/mail/vmail user data (spec invariant I13).
if dpkg --compare-versions "${2:-0}" lt-nl 2.2.0; then
if [ -x /usr/sbin/mailctl ]; then
/usr/sbin/mailctl migrate-config || true
fi
if [ -x /usr/sbin/mail-migrate-to-single-lxc.sh ]; then
/usr/sbin/mail-migrate-to-single-lxc.sh || true
fi
fi
systemctl daemon-reload
systemctl enable secubox-mail.service || true
systemctl start secubox-mail.service || true
fi
#DEBHELPER#
exit 0

View File

@ -0,0 +1,38 @@
secubox-mail (2.2.0-1~bookworm1) bookworm; urgency=medium
Phase 1 source-catch-up + WAF-compliant routing notes.
This release does not change the running mail LXC. It updates the host-
side controllers, packaging metadata, and config schema to match the
canonical paths used by the production board:
LXC: /var/lib/lxc/mail (symlink → /data/lxc/mail)
Data: /data/volumes/mail/{vmail,config,ssl}
Network: unprivileged veth on br-lxc, 10.100.0.10/24
WAF compliance (mandatory per CLAUDE.md):
* HTTPS surface (webmail.<domain>, mail-admin.<domain>) MUST route
via HAProxy → mitmproxy_inspector → backend. This release does not
change the HAProxy frontend ACLs (already in place for
webmail.gk2.secubox.in). The mitmproxy route map must be updated
once at deploy time:
/srv/mitmproxy/haproxy-routes.json
/srv/mitmproxy-in/haproxy-routes.json
"webmail.gk2.secubox.in": ["10.100.0.10", 80]
Then: systemctl restart mitmproxy
* Mail protocols (SMTP 25/587/465, IMAPS 993, ManageSieve 4190) are
TCP pass-through to the LXC via HAProxy. They do not transit
mitmproxy because mitmproxy inspects HTTP, not SMTP/IMAP. This is
not a WAF bypass — mail protocols are not in the WAF's protocol
scope.
Legacy companion packages (secubox-mail-lxc, secubox-webmail,
secubox-webmail-lxc) become transitional metadata-only stubs that
depend on secubox-mail (>= 2.2). Safe to apt-get autoremove after
upgrade.
-- Gerald KERMA <devel@cybermind.fr> Fri, 15 May 2026 12:00:00 +0200

View File

@ -0,0 +1,66 @@
# SecuBox-Deb :: HAProxy mail TCP frontends (Phase 1 rev. 2)
# To activate: append the contents of this file to /etc/haproxy/haproxy.cfg
# after the http/https frontends, then `systemctl reload haproxy`.
#
# Mail protocols (SMTP, IMAPS, ManageSieve) are TCP pass-through to the
# single mail LXC at 10.100.0.10. They do NOT transit mitmproxy because
# mitmproxy inspects HTTP, not SMTP/IMAP. This is not a WAF bypass —
# mail protocols are out of the WAF's protocol scope (per CLAUDE.md).
#
# Postfix/Dovecot inside the LXC present their own TLS certificates;
# HAProxy operates in tcp mode and does not terminate TLS for these.
frontend smtp_in
bind *:25
mode tcp
option tcplog
default_backend smtp_mail
frontend submission_in
bind *:587
mode tcp
option tcplog
default_backend submission_mail
frontend submissions_in
bind *:465
mode tcp
option tcplog
default_backend submissions_mail
frontend imaps_in
bind *:993
mode tcp
option tcplog
default_backend imaps_mail
frontend managesieve_in
bind *:4190
mode tcp
option tcplog
default_backend managesieve_mail
backend smtp_mail
mode tcp
option tcplog
server mail 10.100.0.10:25 check
backend submission_mail
mode tcp
option tcplog
server mail 10.100.0.10:587 check
backend submissions_mail
mode tcp
option tcplog
server mail 10.100.0.10:465 check
backend imaps_mail
mode tcp
option tcplog
server mail 10.100.0.10:993 check
backend managesieve_mail
mode tcp
option tcplog
server mail 10.100.0.10:4190 check

View File

@ -0,0 +1,299 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
# SecuBox-Deb :: mail :: install + configure helpers for the single mail LXC.
# Extracted in Phase 1 from packages/secubox-mail/sbin/{mailserverctl,roundcubectl}.
# Sourced library — do not execute directly.
#
# All functions take the container name as $1. They read defaults from the
# environment ($LXC_BASE, $DATA_PATH, $DOMAIN, $HOSTNAME, $WEBMAIL_PORT) so
# the same helpers work from mailctl, mail-migrate-to-single-lxc.sh, and
# the bats suite (which overrides $LXC_BASE/$DATA_PATH to a tmpdir).
# Bootstrap a fresh Debian bookworm rootfs into ${LXC_BASE}/${container}/rootfs.
# Idempotent: safe to skip if rootfs already exists.
bootstrap_debian() {
local container="$1"
local base="${LXC_BASE:-/var/lib/lxc}"
local lxc_path="$base/$container"
mkdir -p "$lxc_path"
if [ -d "$lxc_path/rootfs/etc" ]; then
echo "[install] rootfs already present at $lxc_path/rootfs — skipping debootstrap"
return 0
fi
if ! command -v debootstrap >/dev/null 2>&1; then
echo "ERROR: debootstrap not installed. Run: apt install debootstrap" >&2
return 1
fi
echo "[install] running debootstrap (a few minutes)..."
debootstrap --variant=minbase --include=ca-certificates,curl,gnupg,locales \
bookworm "$lxc_path/rootfs" http://deb.debian.org/debian
echo "$container" > "$lxc_path/rootfs/etc/hostname"
cat > "$lxc_path/rootfs/etc/resolv.conf" <<'EOF'
nameserver 8.8.8.8
nameserver 1.1.1.1
EOF
echo "[install] Debian base system installed"
}
# Install Postfix + Dovecot + rsyslog inside the LXC rootfs. Run via chroot
# so the container does not need to be running yet.
install_mail_packages() {
local container="$1"
local rootfs="${LXC_BASE:-/var/lib/lxc}/$container/rootfs"
echo "[install] installing Postfix + Dovecot inside $rootfs..."
chroot "$rootfs" /bin/bash <<'CHROOT_EOF'
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
postfix postfix-lmdb \
dovecot-core dovecot-imapd dovecot-pop3d dovecot-lmtpd \
rsyslog ca-certificates openssl
groupadd -g 5000 vmail 2>/dev/null || true
useradd -u 5000 -g vmail -s /usr/sbin/nologin -d /var/mail -M vmail 2>/dev/null || true
apt-get clean
rm -rf /var/lib/apt/lists/*
CHROOT_EOF
echo "[install] mail packages installed"
}
# Install Apache+PHP+Roundcube inside the same LXC rootfs. Mirrors what the
# legacy roundcubectl::install_roundcube_packages did, but the board reality
# uses Apache+mod_php (not nginx+php-fpm). Phase 5 may reconcile.
install_webmail_packages() {
local container="$1"
local rootfs="${LXC_BASE:-/var/lib/lxc}/$container/rootfs"
echo "[install] installing Roundcube webmail stack inside $rootfs..."
chroot "$rootfs" /bin/bash <<'CHROOT_EOF'
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends \
apache2 libapache2-mod-php8.2 \
php8.2-imap php8.2-ldap php8.2-curl php8.2-xml \
php8.2-mbstring php8.2-intl php8.2-sqlite3 \
php8.2-zip php8.2-gd \
roundcube roundcube-core roundcube-plugins roundcube-sqlite3 \
roundcube-skin-classic roundcube-skin-larry \
php-net-sieve \
ca-certificates curl
apt-get clean
rm -rf /var/lib/apt/lists/*
CHROOT_EOF
echo "[install] webmail packages installed"
}
# Write Postfix main.cf + master.cf into the LXC rootfs. Reads $HOSTNAME +
# $DOMAIN from the environment; caller (mailctl) supplies them from
# /etc/secubox/mail.toml.
configure_postfix() {
local container="$1"
local rootfs="${LXC_BASE:-/var/lib/lxc}/$container/rootfs"
local hostname="${HOSTNAME:-mail}"
local domain="${DOMAIN:-secubox.local}"
echo "[install] configuring Postfix in $rootfs..."
mkdir -p "$rootfs/etc/postfix"
cat > "$rootfs/etc/postfix/main.cf" <<EOF
# SecuBox Postfix Configuration
myhostname = ${hostname}.${domain}
mydomain = ${domain}
myorigin = \$mydomain
mydestination = \$myhostname, localhost.\$mydomain, localhost
mynetworks = 127.0.0.0/8 [::1]/128 10.100.0.0/16 192.168.0.0/16 10.0.0.0/8
# Virtual mailbox
virtual_mailbox_domains = ${domain}
virtual_mailbox_base = /var/vmail
virtual_mailbox_maps = lmdb:/etc/mail-config/vmailbox
virtual_alias_maps = lmdb:/etc/mail-config/virtual
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
virtual_transport = lmtp:unix:private/dovecot-lmtp
# SASL auth via Dovecot
smtpd_sasl_auth_enable = yes
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_security_options = noanonymous
broken_sasl_auth_clients = yes
# TLS
smtpd_tls_cert_file = /etc/ssl/mail/fullchain.pem
smtpd_tls_key_file = /etc/ssl/mail/privkey.pem
smtpd_tls_security_level = may
smtp_tls_security_level = may
# Restrictions
smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination
smtpd_sender_restrictions = permit_sasl_authenticated, permit_mynetworks
# Limits
mailbox_size_limit = 0
message_size_limit = 52428800
inet_interfaces = all
inet_protocols = ipv4
EOF
cat > "$rootfs/etc/postfix/master.cf" <<'EOF'
smtp inet n - y - - smtpd
submission inet n - y - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
smtps inet n - y - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
pickup unix n - y 60 1 pickup
cleanup unix n - y - 0 cleanup
qmgr unix n - n 300 1 qmgr
tlsmgr unix - - y 1000? 1 tlsmgr
rewrite unix - - y - - trivial-rewrite
bounce unix - - y - 0 bounce
defer unix - - y - 0 bounce
trace unix - - y - 0 bounce
verify unix - - y - 1 verify
flush unix n - y 1000? 0 flush
proxymap unix - - n - - proxymap
smtp unix - - y - - smtp
relay unix - - y - - smtp
showq unix n - y - - showq
error unix - - y - - error
retry unix - - y - - error
discard unix - - y - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - y - - lmtp
anvil unix - - y - 1 anvil
scache unix - - y - 1 scache
EOF
# Stamp empty lookup tables if not already provided via bind-mount.
[ -e "$rootfs/etc/mail-config/vmailbox" ] || touch "$rootfs/etc/mail-config/vmailbox" 2>/dev/null || true
[ -e "$rootfs/etc/mail-config/virtual" ] || touch "$rootfs/etc/mail-config/virtual" 2>/dev/null || true
echo "[install] Postfix configured"
}
# Write dovecot.conf into the LXC rootfs.
configure_dovecot() {
local container="$1"
local rootfs="${LXC_BASE:-/var/lib/lxc}/$container/rootfs"
echo "[install] configuring Dovecot in $rootfs..."
mkdir -p "$rootfs/etc/dovecot"
cat > "$rootfs/etc/dovecot/dovecot.conf" <<'EOF'
protocols = imap pop3 lmtp
listen = *
mail_location = maildir:/var/vmail/%d/%n
mail_uid = 5000
mail_gid = 5000
first_valid_uid = 500
last_valid_uid = 65534
auth_mechanisms = plain login
passdb {
driver = passwd-file
args = /etc/mail-config/users
}
userdb {
driver = static
args = uid=5000 gid=5000 home=/var/vmail/%d/%n
}
ssl = no
service imap-login {
inet_listener imap { port = 143 }
inet_listener imaps { port = 993; ssl = yes }
}
service pop3-login {
inet_listener pop3 { port = 110 }
inet_listener pop3s { port = 995; ssl = yes }
}
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
}
namespace inbox {
inbox = yes
separator = /
}
log_path = /var/log/dovecot.log
info_log_path = /var/log/dovecot.log
EOF
[ -e "$rootfs/etc/mail-config/users" ] || touch "$rootfs/etc/mail-config/users" 2>/dev/null || true
chmod 644 "$rootfs/etc/mail-config/users" 2>/dev/null || true
echo "[install] Dovecot configured"
}
# Write Apache+Roundcube config inside the LXC rootfs. Phase 1 mirrors what
# the board has today; Phase 5 may migrate to nginx+php-fpm.
configure_roundcube() {
local container="$1"
local rootfs="${LXC_BASE:-/var/lib/lxc}/$container/rootfs"
local domain="${DOMAIN:-secubox.local}"
echo "[install] configuring Roundcube (Apache) in $rootfs..."
mkdir -p "$rootfs/etc/apache2/sites-available" "$rootfs/etc/apache2/sites-enabled"
cat > "$rootfs/etc/apache2/sites-available/roundcube.conf" <<EOF
<VirtualHost *:80>
ServerName webmail.${domain}
DocumentRoot /var/lib/roundcube/public_html
<Directory /var/lib/roundcube/public_html>
Options +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog \${APACHE_LOG_DIR}/roundcube_error.log
CustomLog \${APACHE_LOG_DIR}/roundcube_access.log combined
</VirtualHost>
EOF
chroot "$rootfs" /bin/bash <<'CHROOT_EOF'
a2dissite 000-default 2>/dev/null || true
a2ensite roundcube
a2enmod php8.2 rewrite 2>/dev/null || true
CHROOT_EOF
# Point Roundcube at the local Dovecot + Postfix
if [ -f "$rootfs/etc/roundcube/config.inc.php" ]; then
sed -i \
-e "s|^\$config\['default_host'\].*|\$config['default_host'] = 'tls://localhost';|" \
-e "s|^\$config\['smtp_server'\].*|\$config['smtp_server'] = 'tls://localhost';|" \
"$rootfs/etc/roundcube/config.inc.php" || true
fi
echo "[install] Roundcube (Apache) configured"
}

View File

@ -0,0 +1,83 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
# SecuBox-Deb :: mail :: LXC lifecycle helpers (unprivileged veth on br-lxc).
# Sourced library — do not execute directly.
# Returns 0 if a container's rootfs exists under $LXC_BASE.
lxc_exists() {
local name="$1"
[ -d "${LXC_BASE:-/var/lib/lxc}/$name/rootfs" ]
}
# Returns 0 if a container is currently running.
lxc_running() {
local name="$1"
lxc-info -n "$name" 2>/dev/null | grep -q "State:.*RUNNING"
}
# Render lxc.config for the named container at the given IP.
# Args: name, ipv4 (e.g. "10.100.0.10" or "10.100.0.10/24"),
# bridge (default "br-lxc"), gateway (default "10.100.0.1").
# Writes "$LXC_BASE/$name/config".
lxc_create_config() {
local name="$1"
local ip="$2"
local bridge="${3:-br-lxc}"
local gw="${4:-10.100.0.1}"
local base="${LXC_BASE:-/var/lib/lxc}"
local data="${DATA_PATH:-/data/volumes/mail}"
local ip_cidr
case "$ip" in
*/*) ip_cidr="$ip" ;;
*) ip_cidr="$ip/24" ;;
esac
mkdir -p "$base/$name"
cat > "$base/$name/config" <<EOF
# Generated by mailctl — do not edit by hand
lxc.include = /usr/share/lxc/config/debian.common.conf
lxc.arch = linux64
lxc.uts.name = $name
lxc.rootfs.path = dir:$base/$name/rootfs
lxc.net.0.type = veth
lxc.net.0.link = $bridge
lxc.net.0.flags = up
lxc.net.0.ipv4.address = $ip_cidr
lxc.net.0.ipv4.gateway = $gw
lxc.net.0.name = eth0
lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536
# Bind mounts for persistent data
lxc.mount.entry = $data/vmail var/vmail none bind,create=dir 0 0
lxc.mount.entry = $data/config etc/mail-config none bind,create=dir 0 0
lxc.mount.entry = $data/ssl etc/ssl/mail none bind,create=dir 0 0
lxc.cgroup2.memory.max = 1G
lxc.start.auto = 1
EOF
}
# Start a container and wait until it reports RUNNING (max 10s).
lxc_start_safely() {
local name="$1"
lxc-start -n "$name" -d
local i
for i in 1 2 3 4 5 6 7 8 9 10; do
lxc_running "$name" && return 0
sleep 1
done
return 1
}
# Run a command inside a container; propagate exit status.
lxc_attach_run() {
local name="$1"; shift
lxc-attach -n "$name" -- "$@"
}

View File

@ -0,0 +1,56 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
# SecuBox-Deb :: mail :: Phase 1 defensive migration detectors.
# Sourced library — do not execute directly.
# Detect legacy two-LXC layout under $LXC_BASE. Echoes any legacy
# container names found, one per line. Returns 0 if any, 1 if none.
detect_legacy_lxc() {
local base="${LXC_BASE:-/var/lib/lxc}"
local found=0
local c
for c in mailserver roundcube; do
if [ -d "$base/$c/rootfs" ]; then
echo "$c"
found=1
fi
done
[ "$found" -eq 1 ]
}
# Refuse to touch the data dir if /vmail has any nested content. This
# enforces spec rev. 2 invariant I13 (existing mail data must be preserved).
# Returns 0 if safe to clobber; non-zero with explanation otherwise.
guard_data_path() {
local data="${DATA_PATH:-/data/volumes/mail}"
if [ ! -d "$data/vmail" ]; then
return 0
fi
local count
count=$(find "$data/vmail" -mindepth 2 -print -quit 2>/dev/null | wc -l)
if [ "$count" -gt 0 ]; then
echo "ERROR: $data/vmail already has data — refusing to touch (invariant I13)" >&2
return 1
fi
return 0
}
# Echo any legacy keys present in a mail.toml file (one per line).
# Returns 0 if any legacy key is present, 1 if the file is already migrated.
detect_legacy_toml_keys() {
local toml="$1"
[ -f "$toml" ] || return 1
local found=0
local k
for k in mail_container webmail_container mail_ip webmail_ip webmail_port; do
if grep -q "^${k} *=" "$toml" 2>/dev/null; then
echo "$k"
found=1
fi
done
[ "$found" -eq 1 ]
}

View File

@ -0,0 +1,67 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
#
# Phase 1 rev. 2 migration scanner. Defensive — does NOT create or destroy
# anything by default. Reports legacy state and exits 0 even if legacy
# fragments are found, so debian/postinst can call us without aborting
# the upgrade. Per spec invariant I13, existing /data/volumes/mail/vmail
# data is sacrosanct and never touched.
#
# Invoked from secubox-mail debian/postinst on upgrade from < 2.2.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LIB_DIR="${LIB_DIR:-/usr/lib/secubox/mail/lib}"
[ -d "$LIB_DIR" ] || LIB_DIR="$SCRIPT_DIR/../lib"
# shellcheck source=/dev/null
source "$LIB_DIR/lxc.sh"
# shellcheck source=/dev/null
source "$LIB_DIR/migrate.sh"
log() { echo "[mail-migrate] $*"; }
warn() { echo "[mail-migrate][WARN] $*" >&2; }
main() {
[ "$(id -u)" -eq 0 ] || { echo "must run as root" >&2; exit 1; }
: "${LXC_BASE:=/var/lib/lxc}"
: "${DATA_PATH:=/data/volumes/mail}"
log "Phase 1 scan starting (LXC_BASE=$LXC_BASE, DATA_PATH=$DATA_PATH)"
# 1. Legacy LXCs (rev. 1 two-LXC layout). Not expected on this board.
if legacy=$(detect_legacy_lxc 2>/dev/null); then
warn "legacy LXC dirs present:"
echo "$legacy" | sed 's/^/[mail-migrate][WARN] - /' >&2
warn "these are NOT removed automatically. To archive:"
warn " cd $LXC_BASE && tar -czf /tmp/legacy-mail-lxc-\$(date +%s).tar.gz mailserver roundcube"
else
log "no legacy mailserver/roundcube LXC dirs — clean"
fi
# 2. Data path — enforce invariant I13.
if [ -d "$DATA_PATH/vmail" ]; then
if guard_data_path 2>/dev/null; then
log "data path $DATA_PATH/vmail is empty — fresh start ok"
else
log "data path $DATA_PATH/vmail has user data — preserving (per spec I13)"
fi
else
log "data path $DATA_PATH/vmail does not exist yet"
fi
# 3. mail LXC presence.
if lxc_exists "mail"; then
log "mail LXC present at $LXC_BASE/mail"
else
warn "mail LXC not yet installed — run 'mailctl install' after this scanner"
fi
log "scan complete — no destructive actions taken"
}
main "$@"

View File

@ -3,11 +3,11 @@
# Unified mail + webmail management for Debian
# Three-fold architecture: Components, Status, Access
VERSION="1.3.0"
VERSION="2.2.0"
CONFIG_FILE="/etc/secubox/mail.toml"
LIB_DIR="/usr/lib/secubox/mail/lib"
DATA_PATH="/srv/mail"
LXC_PATH="/srv/lxc"
DATA_PATH="/data/volumes/mail"
LXC_PATH="/var/lib/lxc"
# Colors
RED='\033[0;31m'
@ -41,10 +41,12 @@ config_get() {
DOMAIN=$(config_get "domain" "secubox.local")
HOSTNAME=$(config_get "hostname" "mail")
MAIL_CONTAINER=$(config_get "mail_container" "mailserver")
WEBMAIL_CONTAINER=$(config_get "webmail_container" "roundcube")
MAIL_IP=$(config_get "mail_ip" "192.168.255.30")
WEBMAIL_PORT=$(config_get "webmail_port" "8027")
CONTAINER=$(config_get "container" "mail")
WEBMAIL_CONTAINER="$CONTAINER" # legacy alias — webmail merged into single container
LXC_IP=$(config_get "lxc_ip" "10.100.0.10")
LXC_BRIDGE=$(config_get "lxc_bridge" "br-lxc")
LXC_GATEWAY=$(config_get "lxc_gateway" "10.100.0.1")
WEBMAIL_PORT=80 # Roundcube now on standard HTTP inside the LXC, proxied via host nginx :443
# ============================================================================
# LXC Helpers
@ -77,8 +79,8 @@ cmd_components() {
local webmail_installed=false
local webmail_running=false
lxc_exists "$MAIL_CONTAINER" && mail_installed=true
lxc_running "$MAIL_CONTAINER" && mail_running=true
lxc_exists "$CONTAINER" && mail_installed=true
lxc_running "$CONTAINER" && mail_running=true
lxc_exists "$WEBMAIL_CONTAINER" && webmail_installed=true
lxc_running "$WEBMAIL_CONTAINER" && webmail_running=true
@ -88,12 +90,12 @@ cmd_components() {
{
"name": "Mail Server",
"type": "lxc",
"container": "$MAIL_CONTAINER",
"container": "$CONTAINER",
"description": "Postfix + Dovecot mail server",
"installed": $mail_installed,
"running": $mail_running,
"ports": [25, 587, 465, 143, 993, 110, 995],
"ip": "$MAIL_IP"
"ip": "$LXC_IP"
},
{
"name": "Webmail",
@ -209,13 +211,13 @@ cmd_user_repair() {
local email="$1"
[ -z "$email" ] && { echo "Usage: mailctl user-repair <email>"; return 1; }
if ! lxc_running "$MAIL_CONTAINER"; then
if ! lxc_running "$CONTAINER"; then
error "Mail container not running"
return 1
fi
log "Repairing mailbox for $email..."
lxc_attach "$MAIL_CONTAINER" doveadm force-resync -u "$email" '*'
lxc_attach "$CONTAINER" doveadm force-resync -u "$email" '*'
log "Mailbox repair complete"
}
@ -224,13 +226,13 @@ cmd_user_repair() {
# ============================================================================
cmd_fix_ports() {
if ! lxc_running "$MAIL_CONTAINER"; then
if ! lxc_running "$CONTAINER"; then
error "Mail container not running"
return 1
fi
log "Checking mail ports..."
local ports=$(lxc_attach "$MAIL_CONTAINER" netstat -tln 2>/dev/null)
local ports=$(lxc_attach "$CONTAINER" netstat -tln 2>/dev/null)
local all_ok=true
for port in 25 587 465 143 993; do
@ -244,7 +246,7 @@ cmd_fix_ports() {
if [ "$all_ok" = "false" ]; then
warn "Some ports are not listening. Attempting restart..."
lxc_attach "$MAIL_CONTAINER" /opt/start-mail.sh &
lxc_attach "$CONTAINER" /opt/start-mail.sh &
sleep 3
log "Services restarted"
else
@ -288,9 +290,9 @@ cmd_uninstall() {
systemctl stop secubox-mail 2>/dev/null
log "Removing containers..."
lxc-stop -n "$MAIL_CONTAINER" 2>/dev/null
lxc-stop -n "$CONTAINER" 2>/dev/null
lxc-stop -n "$WEBMAIL_CONTAINER" 2>/dev/null
rm -rf "$LXC_PATH/$MAIL_CONTAINER"
rm -rf "$LXC_PATH/$CONTAINER"
rm -rf "$LXC_PATH/$WEBMAIL_CONTAINER"
log "Mail server removed. Data preserved in $DATA_PATH"
@ -330,15 +332,15 @@ cmd_status() {
echo "Configuration:"
echo " Domain: $DOMAIN"
echo " Hostname: $HOSTNAME.$DOMAIN"
echo " Mail IP: $MAIL_IP"
echo " Mail IP: $LXC_IP"
echo ""
# Mail server status
echo "Mail Server ($MAIL_CONTAINER):"
if lxc_running "$MAIL_CONTAINER"; then
echo "Mail Server ($CONTAINER):"
if lxc_running "$CONTAINER"; then
echo -e " Status: ${GREEN}Running${NC}"
# Check ports
local ports=$(lxc_attach "$MAIL_CONTAINER" netstat -tln 2>/dev/null)
local ports=$(lxc_attach "$CONTAINER" netstat -tln 2>/dev/null)
for port in 25 587 465 993 995; do
if echo "$ports" | grep -q ":$port "; then
echo -e " Port $port: ${GREEN}listening${NC}"
@ -346,7 +348,7 @@ cmd_status() {
echo -e " Port $port: ${RED}closed${NC}"
fi
done
elif lxc_exists "$MAIL_CONTAINER"; then
elif lxc_exists "$CONTAINER"; then
echo -e " Status: ${YELLOW}Stopped${NC}"
else
echo -e " Status: ${RED}Not installed${NC}"
@ -655,8 +657,8 @@ cmd_dkim() {
cmd_logs() {
local lines="${1:-50}"
if lxc_running "$MAIL_CONTAINER"; then
lxc_attach "$MAIL_CONTAINER" tail -n "$lines" /var/log/mail.log 2>/dev/null
if lxc_running "$CONTAINER"; then
lxc_attach "$CONTAINER" tail -n "$lines" /var/log/mail.log 2>/dev/null
else
error "Mail container not running"
fi
@ -724,6 +726,57 @@ Examples:
EOF
}
# ============================================================================
# Phase 1 config migration (rev. 2)
# ============================================================================
cmd_migrate_config() {
[ "$(id -u)" -eq 0 ] || { error "Root required"; return 1; }
: "${LIB_DIR:=/usr/lib/secubox/mail/lib}"
[ -d "$LIB_DIR" ] || LIB_DIR="$(dirname "$0")/../lib"
# shellcheck source=/dev/null
source "$LIB_DIR/migrate.sh"
local cfg="${CONFIG_FILE:-/etc/secubox/mail.toml}"
[ -f "$cfg" ] || { warn "no config to migrate at $cfg"; return 0; }
if grep -q "^container *=" "$cfg" 2>/dev/null; then
log "config already migrated (has 'container =' key)"
return 0
fi
if ! detect_legacy_toml_keys "$cfg" >/dev/null; then
log "no legacy keys to migrate"
return 0
fi
local backup="${cfg}.pre-phase1.$(date +%s).bak"
cp "$cfg" "$backup"
log "backup written to $backup"
python3 - "$cfg" <<'PY'
import sys, re
path = sys.argv[1]
src = open(path).read()
inject = (
'container = "mail"\n'
'lxc_ip = "10.100.0.10"\n'
'lxc_bridge = "br-lxc"\n'
'lxc_gateway = "10.100.0.1"\n'
'data_path = "/data/volumes/mail"\n'
'lxc_path = "/var/lib/lxc"\n'
)
# Comment out legacy keys FIRST so we don't end up with duplicates of
# data_path / lxc_path after injection.
for k in ("mail_container", "webmail_container", "mail_ip", "webmail_ip",
"webmail_port", "data_path", "lxc_path"):
src = re.sub(rf"^({k} *=.*)$", r"# DEPRECATED Phase 1: \1", src, flags=re.MULTILINE)
src = re.sub(r"(\[mail\]\n)", r"\1" + inject, src, count=1)
open(path, "w").write(src)
PY
log "config migrated to single-container schema"
}
# ============================================================================
# Main
# ============================================================================
@ -737,6 +790,7 @@ case "${1:-}" in
install) shift; cmd_install "$@" ;;
uninstall) shift; cmd_uninstall "$@" ;;
migrate) shift; cmd_migrate "$@" ;;
migrate-config) shift; cmd_migrate_config "$@" ;;
# Service control
start) shift; cmd_start "$@" ;;
stop) shift; cmd_stop "$@" ;;

File diff suppressed because it is too large Load Diff

View File

@ -1,395 +1,10 @@
#!/bin/bash
# SecuBox Roundcube Webmail Controller
# LXC container management for Roundcube (Debian bookworm)
VERSION="1.4.0"
CONFIG_FILE="/etc/secubox/mail.toml"
CONTAINER="roundcube"
LXC_BASE="/srv/lxc"
LXC_PATH="$LXC_BASE/roundcube"
DATA_PATH="/srv/mail"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[ROUNDCUBE]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
config_get() {
local key="$1" default="$2"
[ -f "$CONFIG_FILE" ] && grep "^${key} *=" "$CONFIG_FILE" 2>/dev/null | head -1 | cut -d= -f2- | tr -d ' "' || echo "$default"
}
DOMAIN=$(config_get "domain" "secubox.local")
MAIL_HOST=$(config_get "hostname" "mail")
WEBMAIL_PORT=$(config_get "webmail_port" "8027")
require_root() {
[ "$(id -u)" -eq 0 ] || { error "Root required"; exit 1; }
}
lxc_running() { lxc-info -n "$CONTAINER" -P "$LXC_BASE" 2>/dev/null | grep -q "State:.*RUNNING"; }
lxc_exists() { [ -d "$LXC_PATH/rootfs" ]; }
# ============================================================================
# LXC Configuration
# ============================================================================
create_lxc_config() {
mkdir -p "$LXC_PATH"
cat > "$LXC_PATH/config" << EOF
lxc.uts.name = roundcube
lxc.rootfs.path = dir:${LXC_PATH}/rootfs
lxc.net.0.type = none
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
lxc.cap.drop = sys_module mac_admin mac_override sys_time
lxc.tty.max = 4
lxc.pty.max = 256
lxc.cgroup2.memory.max = 512M
lxc.init.cmd = /opt/start-roundcube.sh
EOF
}
create_startup_script() {
mkdir -p "$LXC_PATH/rootfs/opt"
cat > "$LXC_PATH/rootfs/opt/start-roundcube.sh" << 'EOF'
#!/bin/bash
# Roundcube startup (Debian)
# Start PHP-FPM
service php8.2-fpm start
# Start nginx
service nginx start
echo "Roundcube started on port 8027"
# Keep container running
exec tail -f /var/log/nginx/error.log 2>/dev/null || exec sleep infinity
EOF
chmod +x "$LXC_PATH/rootfs/opt/start-roundcube.sh"
}
# ============================================================================
# Debian Bootstrap
# ============================================================================
bootstrap_debian() {
require_root
log "Bootstrapping Debian bookworm for Roundcube..."
mkdir -p "$LXC_PATH"
if ! command -v debootstrap &>/dev/null; then
error "debootstrap not installed. Run: apt install debootstrap"
exit 1
fi
log "Running debootstrap (this takes a few minutes)..."
debootstrap --variant=minbase bookworm "$LXC_PATH/rootfs" http://deb.debian.org/debian
# Configure basic system
echo "roundcube" > "$LXC_PATH/rootfs/etc/hostname"
cat > "$LXC_PATH/rootfs/etc/resolv.conf" << 'EOF'
nameserver 8.8.8.8
nameserver 1.1.1.1
EOF
log "Debian base system installed"
}
install_roundcube_packages() {
require_root
local rootfs="$LXC_PATH/rootfs"
log "Installing Roundcube packages..."
# Install via chroot since container may not be running yet
chroot "$rootfs" /bin/bash << 'CHROOT_EOF'
export DEBIAN_FRONTEND=noninteractive
# Update and install packages
apt-get update
apt-get install -y --no-install-recommends \
nginx \
php8.2-fpm php8.2-imap php8.2-ldap php8.2-curl \
php8.2-xml php8.2-mbstring php8.2-intl php8.2-sqlite3 \
php8.2-zip php8.2-gd \
ca-certificates curl unzip
# Download Roundcube
cd /tmp
curl -L -o roundcube.tar.gz https://github.com/roundcube/roundcubemail/releases/download/1.6.6/roundcubemail-1.6.6-complete.tar.gz
mkdir -p /var/www/roundcube
tar -xzf roundcube.tar.gz -C /var/www/roundcube --strip-components=1
chown -R www-data:www-data /var/www/roundcube
rm -f roundcube.tar.gz
# Initialize SQLite database
mkdir -p /var/www/roundcube/db
mkdir -p /var/www/roundcube/temp
mkdir -p /var/www/roundcube/logs
chown -R www-data:www-data /var/www/roundcube
# Clean up
apt-get clean
rm -rf /var/lib/apt/lists/*
CHROOT_EOF
log "Packages installed"
}
configure_roundcube() {
local rootfs="$LXC_PATH/rootfs"
log "Configuring Roundcube..."
# Nginx config - use specific port for host networking
mkdir -p "$rootfs/etc/nginx/sites-available"
mkdir -p "$rootfs/etc/nginx/sites-enabled"
cat > "$rootfs/etc/nginx/sites-available/roundcube" << EOF
server {
listen ${WEBMAIL_PORT};
server_name webmail.$DOMAIN localhost;
root /var/www/roundcube;
index index.php;
location / {
try_files \$uri \$uri/ /index.php?\$args;
}
location ~ \.php\$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\. {
deny all;
}
}
EOF
# Enable site
rm -f "$rootfs/etc/nginx/sites-enabled/default"
ln -sf /etc/nginx/sites-available/roundcube "$rootfs/etc/nginx/sites-enabled/roundcube"
# Roundcube config - use localhost since container shares host network
cat > "$rootfs/var/www/roundcube/config/config.inc.php" << EOF
<?php
\$config['db_dsnw'] = 'sqlite:////var/www/roundcube/db/sqlite.db';
\$config['imap_host'] = 'localhost:143';
\$config['smtp_host'] = 'localhost:587';
\$config['smtp_user'] = '%u';
\$config['smtp_pass'] = '%p';
\$config['product_name'] = 'SecuBox Webmail';
\$config['des_key'] = '$(openssl rand -base64 24)';
\$config['plugins'] = ['archive', 'zipdownload'];
\$config['language'] = 'fr_FR';
\$config['skin'] = 'elastic';
\$config['smtp_conn_options'] = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]];
\$config['imap_conn_options'] = ['ssl' => ['verify_peer' => false, 'verify_peer_name' => false]];
EOF
log "Roundcube configured"
}
# ============================================================================
# Commands
# ============================================================================
cmd_install() {
require_root
log "Installing Roundcube Webmail LXC (Debian bookworm)..."
if ! lxc_exists; then
bootstrap_debian
fi
install_roundcube_packages
configure_roundcube
create_lxc_config
create_startup_script
log "Roundcube installed!"
}
cmd_start() {
require_root
if lxc_running; then
log "Roundcube already running"
return 0
fi
if ! lxc_exists; then
error "Roundcube not installed. Run 'roundcubectl install' first"
return 1
fi
create_lxc_config
log "Starting Roundcube..."
lxc-start -n "$CONTAINER" -P "$LXC_BASE" -d
sleep 3
if lxc_running; then
log "Roundcube started at http://localhost:$WEBMAIL_PORT"
else
error "Failed to start"
return 1
fi
}
cmd_stop() {
require_root
if ! lxc_running; then
log "Roundcube is not running"
return 0
fi
log "Stopping Roundcube..."
lxc-stop -n "$CONTAINER" -P "$LXC_BASE"
log "Stopped"
}
cmd_restart() {
cmd_stop
sleep 2
cmd_start
}
cmd_status() {
echo ""
echo "Roundcube Webmail LXC v$VERSION"
echo "================================"
echo ""
echo "Container: $CONTAINER"
echo "Base OS: Debian bookworm"
if lxc_running; then
echo -e "Status: ${GREEN}Running${NC}"
echo "Port: $WEBMAIL_PORT"
echo "URL: http://localhost:$WEBMAIL_PORT"
elif lxc_exists; then
echo -e "Status: ${YELLOW}Stopped${NC}"
else
echo -e "Status: ${RED}Not installed${NC}"
fi
}
cmd_shell() {
if ! lxc_running; then
error "Container not running"
return 1
fi
lxc-attach -n "$CONTAINER" -P "$LXC_BASE" -- /bin/bash
}
# ============================================================================
# THREE-FOLD ARCHITECTURE: Components (What)
# ============================================================================
cmd_components() {
local installed=false
local running=false
lxc_exists && installed=true
lxc_running && running=true
cat <<EOF
{
"components": [
{
"name": "Roundcube Webmail",
"type": "lxc",
"container": "$CONTAINER",
"description": "Webmail interface for mail server",
"os": "debian-bookworm",
"installed": $installed,
"running": $running,
"port": $WEBMAIL_PORT
},
{
"name": "Nginx",
"type": "service",
"description": "Web server for Roundcube",
"parent": "$CONTAINER"
},
{
"name": "PHP-FPM",
"type": "service",
"description": "PHP 8.2 processor for Roundcube",
"parent": "$CONTAINER"
}
]
}
EOF
}
# ============================================================================
# THREE-FOLD ARCHITECTURE: Access (How to connect)
# ============================================================================
cmd_access() {
local running=false
lxc_running && running=true
cat <<EOF
{
"webmail": {
"url": "https://webmail.$DOMAIN",
"local_url": "http://localhost:$WEBMAIL_PORT",
"running": $running
},
"mail_server": {
"imap": "localhost:143",
"smtp": "localhost:587"
},
"admin_panel": "/mail/"
}
EOF
}
show_help() {
cat << EOF
Roundcube Webmail LXC Controller v$VERSION
Debian bookworm based container
Three-fold architecture: Components, Status, Access
Usage: roundcubectl <command>
Information (Three-fold):
components List system components (JSON)
status Show status
access Show connection URLs (JSON)
Service:
install Install LXC container (Debian)
start Start Roundcube
stop Stop Roundcube
restart Restart
shell Open shell in container
EOF
}
case "${1:-}" in
# Three-fold
components) cmd_components ;;
access) cmd_access ;;
# Service
install) shift; cmd_install "$@" ;;
start) shift; cmd_start "$@" ;;
stop) shift; cmd_stop "$@" ;;
restart) shift; cmd_restart "$@" ;;
status) shift; cmd_status "$@" ;;
shell) shift; cmd_shell "$@" ;;
help|--help|-h|'') show_help ;;
*) error "Unknown: $1"; exit 1 ;;
esac
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# DEPRECATED in secubox-mail 2.2 — Roundcube now lives inside the single
# 'mail' LXC. This shim forwards to mailctl. Will be removed in 3.0.
set -euo pipefail
echo "[roundcubectl] DEPRECATED — forwarding to mailctl (will be removed in 3.0)" >&2
exec /usr/sbin/mailctl "$@"

View File

@ -0,0 +1,18 @@
# SecuBox-Deb :: mail :: bats shared fixtures (Phase 1).
# Sourced by tests/*.bats via `load helpers`.
load_libs() {
local pkg_root="${BATS_TEST_DIRNAME}/.."
# shellcheck source=/dev/null
source "${pkg_root}/lib/lxc.sh"
# shellcheck source=/dev/null
source "${pkg_root}/lib/install.sh"
# shellcheck source=/dev/null
source "${pkg_root}/lib/migrate.sh"
}
make_fake_lxc_env() {
export LXC_BASE="$BATS_TEST_TMPDIR/lxc"
export DATA_PATH="$BATS_TEST_TMPDIR/data-volumes-mail"
mkdir -p "$LXC_BASE" "$DATA_PATH"
}

View File

@ -0,0 +1,21 @@
#!/usr/bin/env bats
load helpers
setup() { load_libs; make_fake_lxc_env; }
@test "install.sh sources cleanly" {
[ "$(type -t bootstrap_debian)" = "function" ]
[ "$(type -t install_mail_packages)" = "function" ]
[ "$(type -t install_webmail_packages)" = "function" ]
[ "$(type -t configure_postfix)" = "function" ]
[ "$(type -t configure_dovecot)" = "function" ]
[ "$(type -t configure_roundcube)" = "function" ]
}
@test "bootstrap_debian refuses to run if debootstrap missing" {
local fake_path="$BATS_TEST_TMPDIR/path"
mkdir -p "$fake_path"
PATH="$fake_path" run bootstrap_debian "$LXC_BASE/mail"
[ "$status" -ne 0 ]
[[ "$output" == *"debootstrap"* ]]
}

View File

@ -0,0 +1,41 @@
#!/usr/bin/env bats
load helpers
setup() { load_libs; make_fake_lxc_env; }
@test "lxc.sh sources cleanly" {
[ "$(type -t lxc_exists)" = "function" ]
[ "$(type -t lxc_running)" = "function" ]
[ "$(type -t lxc_create_config)" = "function" ]
[ "$(type -t lxc_start_safely)" = "function" ]
[ "$(type -t lxc_attach_run)" = "function" ]
}
@test "lxc_exists returns 1 for missing container" {
run lxc_exists "ghost-mail"
[ "$status" -eq 1 ]
}
@test "lxc_exists returns 0 when rootfs exists" {
mkdir -p "$LXC_BASE/mail/rootfs"
run lxc_exists "mail"
[ "$status" -eq 0 ]
}
@test "lxc_create_config writes a config with veth + 10.100.0.10 + br-lxc + unprivileged" {
lxc_create_config "mail" "10.100.0.10" "br-lxc" "10.100.0.1"
[ -f "$LXC_BASE/mail/config" ]
grep -q "lxc.uts.name = mail" "$LXC_BASE/mail/config"
grep -q "lxc.rootfs.path = dir:$LXC_BASE/mail/rootfs" "$LXC_BASE/mail/config"
grep -q "lxc.net.0.type = veth" "$LXC_BASE/mail/config"
grep -q "lxc.net.0.link = br-lxc" "$LXC_BASE/mail/config"
grep -q "lxc.net.0.ipv4.address = 10.100.0.10/24" "$LXC_BASE/mail/config"
grep -q "lxc.net.0.ipv4.gateway = 10.100.0.1" "$LXC_BASE/mail/config"
grep -q "lxc.idmap = u 0 100000 65536" "$LXC_BASE/mail/config"
grep -qE "vmail[[:space:]]+var/vmail[[:space:]]+none[[:space:]]+bind" "$LXC_BASE/mail/config"
}
@test "lxc_create_config accepts plain IP (no /CIDR) and defaults to /24" {
lxc_create_config "mail" "10.100.0.10"
grep -q "lxc.net.0.ipv4.address = 10.100.0.10/24" "$LXC_BASE/mail/config"
}

View File

@ -0,0 +1,73 @@
#!/usr/bin/env bats
load helpers
setup() { load_libs; make_fake_lxc_env; }
@test "migrate.sh sources cleanly" {
[ "$(type -t detect_legacy_lxc)" = "function" ]
[ "$(type -t guard_data_path)" = "function" ]
[ "$(type -t detect_legacy_toml_keys)" = "function" ]
}
@test "detect_legacy_lxc finds mailserver+roundcube paths" {
mkdir -p "$LXC_BASE/mailserver/rootfs" "$LXC_BASE/roundcube/rootfs"
run detect_legacy_lxc
[ "$status" -eq 0 ]
[[ "$output" == *"mailserver"* ]]
[[ "$output" == *"roundcube"* ]]
}
@test "detect_legacy_lxc returns 1 when only 'mail' exists" {
mkdir -p "$LXC_BASE/mail/rootfs"
run detect_legacy_lxc
[ "$status" -eq 1 ]
}
@test "guard_data_path refuses non-empty vmail (data preservation invariant I13)" {
mkdir -p "$DATA_PATH/vmail/secubox.in/gk2"
touch "$DATA_PATH/vmail/secubox.in/gk2/keep-me"
run guard_data_path
[ "$status" -ne 0 ]
[[ "$output" == *"refusing"* ]] || [[ "$output" == *"already has data"* ]]
}
@test "guard_data_path accepts empty vmail dir" {
mkdir -p "$DATA_PATH/vmail"
run guard_data_path
[ "$status" -eq 0 ]
}
@test "guard_data_path accepts missing vmail dir" {
run guard_data_path
[ "$status" -eq 0 ]
}
@test "detect_legacy_toml_keys finds each legacy key" {
local toml="$BATS_TEST_TMPDIR/mail.toml"
cat > "$toml" <<TOML
[mail]
mail_container = "mailserver"
webmail_container = "roundcube"
mail_ip = "192.168.255.30"
webmail_ip = "192.168.255.31"
webmail_port = 8027
TOML
run detect_legacy_toml_keys "$toml"
[ "$status" -eq 0 ]
[[ "$output" == *"mail_container"* ]]
[[ "$output" == *"webmail_container"* ]]
[[ "$output" == *"mail_ip"* ]]
[[ "$output" == *"webmail_ip"* ]]
[[ "$output" == *"webmail_port"* ]]
}
@test "detect_legacy_toml_keys returns 1 when toml is already migrated" {
local toml="$BATS_TEST_TMPDIR/mail.toml"
cat > "$toml" <<TOML
[mail]
container = "mail"
lxc_ip = "10.100.0.10"
TOML
run detect_legacy_toml_keys "$toml"
[ "$status" -eq 1 ]
}

View File

@ -1,3 +1,10 @@
secubox-webmail-lxc (2.2.0-1~bookworm1) bookworm; urgency=medium
* Transitional package — all functionality moved to secubox-mail >= 2.2.
* Closes: #136
-- Gerald KERMA <devel@cybermind.fr> Fri, 15 May 2026 12:00:00 +0200
secubox-webmail-lxc (1.1.0-1~bookworm1) bookworm; urgency=medium
* Remove standalone menu entry (now integrated into secubox-mail UI)

View File

@ -1,5 +1,5 @@
Source: secubox-webmail-lxc
Section: admin
Section: oldlibs
Priority: optional
Maintainer: Gerald KERMA <devel@cybermind.fr>
Build-Depends: debhelper-compat (= 13)
@ -7,15 +7,8 @@ Standards-Version: 4.6.2
Package: secubox-webmail-lxc
Architecture: all
Depends: ${misc:Depends}, secubox-core (>= 1.0.0), lxc, wget
Recommends: secubox-mail, secubox-mail-lxc
Description: SecuBox Webmail LXC Container (Backend)
LXC container management for Roundcube webmail.
Backend component consumed by secubox-mail (no standalone UI).
.
Provides Alpine Linux container with:
- Roundcube webmail
- Nginx + PHP-FPM
- Auto-configuration for mail server
.
Install secubox-mail for the management UI.
Depends: ${misc:Depends}, secubox-mail (>= 2.2)
Description: Transitional package — webmail LXC functionality moved to secubox-mail
Roundcube webmail now lives inside the single 'mail' LXC managed by
secubox-mail (>= 2.2). This package ships no files. Safe to
apt-get autoremove after upgrade.

View File

@ -1,9 +1,18 @@
#!/bin/sh
set -e
# Transitional package (secubox-webmail-lxc 2.2.0) — clean up the old
# standalone service if it's still around from <2.2 installs.
if [ "$1" = "configure" ]; then
systemctl daemon-reload
systemctl enable secubox-webmail-lxc.service || true
systemctl start secubox-webmail-lxc.service || true
if [ -e /lib/systemd/system/secubox-webmail-lxc.service ] \
|| [ -e /etc/systemd/system/secubox-webmail-lxc.service ]; then
systemctl stop secubox-webmail-lxc.service 2>/dev/null || true
systemctl disable secubox-webmail-lxc.service 2>/dev/null || true
rm -f /lib/systemd/system/secubox-webmail-lxc.service \
/etc/systemd/system/secubox-webmail-lxc.service
systemctl daemon-reload || true
fi
fi
#DEBHELPER#
exit 0

View File

@ -1,9 +1,5 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
systemctl stop secubox-webmail-lxc.service || true
systemctl disable secubox-webmail-lxc.service || true
systemctl reload nginx 2>/dev/null || true
fi
# Transitional package — nothing to undo.
#DEBHELPER#
exit 0

View File

@ -1,14 +1,10 @@
#!/usr/bin/make -f
# Transitional package — ships no files.
%:
dh $@
override_dh_auto_install:
# API (used by secubox-mail for container management)
install -d debian/secubox-webmail-lxc/usr/lib/secubox/webmail-lxc
cp -r api debian/secubox-webmail-lxc/usr/lib/secubox/webmail-lxc/
# Control scripts
install -d debian/secubox-webmail-lxc/usr/sbin
[ -d sbin ] && install -m 755 sbin/* debian/secubox-webmail-lxc/usr/sbin/ || true
# Modular nginx config (API proxy only, no separate menu)
install -d debian/secubox-webmail-lxc/etc/nginx/secubox.d
[ -f nginx/webmail-lxc.conf ] && cp nginx/webmail-lxc.conf debian/secubox-webmail-lxc/etc/nginx/secubox.d/ || true
:
override_dh_installsystemd:
:

View File

@ -1,6 +1,3 @@
# /etc/nginx/secubox.d/webmail-lxc.conf
# Installed by secubox-webmail-lxc package
location /api/v1/webmail-lxc/ {
proxy_pass http://unix:/run/secubox/webmail-lxc.sock:/;
include /etc/nginx/snippets/secubox-proxy.conf;
}
# DEPRECATED in secubox-webmail-lxc 2.2 — webmail LXC management folded
# into secubox-mail's /api/v1/mail/. Empty for one release.

View File

@ -1,3 +1,10 @@
secubox-webmail (2.2.0-1~bookworm1) bookworm; urgency=medium
* Transitional package — all functionality moved to secubox-mail >= 2.2.
* Closes: #136
-- Gerald KERMA <devel@cybermind.fr> Fri, 15 May 2026 12:00:00 +0200
secubox-webmail (1.0.0-1~bookworm1) bookworm; urgency=medium
* Initial release

View File

@ -1,5 +1,5 @@
Source: secubox-webmail
Section: admin
Section: oldlibs
Priority: optional
Maintainer: Gerald KERMA <devel@cybermind.fr>
Build-Depends: debhelper-compat (= 13)
@ -7,15 +7,7 @@ Standards-Version: 4.6.2
Package: secubox-webmail
Architecture: all
Depends: ${misc:Depends}, secubox-core (>= 1.0.0)
Recommends: secubox-webmail-lxc
Description: SecuBox Webmail Module
Webmail management API and dashboard.
Layer 2 of the 2-layer webmail architecture.
.
Features:
- Auto-detect Roundcube/SOGo
- Service control
- Cache management
.
For containerized deployment, install secubox-webmail-lxc.
Depends: ${misc:Depends}, secubox-mail (>= 2.2)
Description: Transitional package — webmail admin folded into secubox-mail
The webmail admin API is now part of secubox-mail (>= 2.2). This
package ships no files. Safe to apt-get autoremove after upgrade.

View File

@ -1,9 +1,18 @@
#!/bin/sh
set -e
# Transitional package (secubox-webmail 2.2.0) — clean up the old
# standalone service + www tree if they're still around from <2.2.
if [ "$1" = "configure" ]; then
systemctl daemon-reload
systemctl enable secubox-webmail.service || true
systemctl start secubox-webmail.service || true
if [ -e /lib/systemd/system/secubox-webmail.service ] \
|| [ -e /etc/systemd/system/secubox-webmail.service ]; then
systemctl stop secubox-webmail.service 2>/dev/null || true
systemctl disable secubox-webmail.service 2>/dev/null || true
rm -f /lib/systemd/system/secubox-webmail.service \
/etc/systemd/system/secubox-webmail.service
systemctl daemon-reload || true
fi
fi
#DEBHELPER#
exit 0

View File

@ -1,9 +1,5 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
systemctl stop secubox-webmail.service || true
systemctl disable secubox-webmail.service || true
systemctl reload nginx 2>/dev/null || true
fi
# Transitional package — nothing to undo.
#DEBHELPER#
exit 0

View File

@ -1,14 +1,10 @@
#!/usr/bin/make -f
# Transitional package — ships no files.
%:
dh $@
override_dh_auto_install:
install -d debian/secubox-webmail/usr/lib/secubox/webmail
cp -r api debian/secubox-webmail/usr/lib/secubox/webmail/
install -d debian/secubox-webmail/usr/share/secubox/www/webmail
cp -r www/webmail/. debian/secubox-webmail/usr/share/secubox/www/webmail/
install -d debian/secubox-webmail/usr/share/secubox/menu.d
[ -d menu.d ] && cp -r menu.d/. debian/secubox-webmail/usr/share/secubox/menu.d/ || true
# Modular nginx config
install -d debian/secubox-webmail/etc/nginx/secubox.d
[ -f nginx/webmail.conf ] && cp nginx/webmail.conf debian/secubox-webmail/etc/nginx/secubox.d/ || true
:
override_dh_installsystemd:
:

View File

@ -1,6 +1,4 @@
# /etc/nginx/secubox.d/webmail.conf
# Installed by secubox-webmail package
location /api/v1/webmail/ {
proxy_pass http://unix:/run/secubox/webmail.sock:/;
include /etc/nginx/snippets/secubox-proxy.conf;
}
# DEPRECATED in secubox-webmail 2.2 — webmail admin API is folded into
# secubox-mail's /api/v1/mail/. This snippet is kept empty for one release
# so a partial-upgrade nginx reload doesn't error on a missing include.

View File

@ -284,9 +284,14 @@ gadget_start() {
log "Activation sur UDC: ${udc}"
echo "$udc" > UDC
# Attendre que l'interface usb1 (ECM) apparaisse
# Note: Le gadget composite crée usb0 (RNDIS/Windows) et usb1 (ECM/Linux-Mac)
# On configure usb1 car les hôtes Linux utilisent le driver cdc_ether (ECM)
# Attendre que la deuxième interface réseau du gadget composé
# apparaisse côté kernel. Le composé crée rndis.usb0 puis ecm.usb0
# (dans cet ordre, cf. liens configfs ci-dessus) — le kernel les
# enregistre comme /sys/class/net/usb0 et /sys/class/net/usb1.
# Côté hôte, RNDIS et ECM partagent le même host_addr donc l'ARP
# remonte sur n'importe laquelle des deux interfaces UP du host;
# binder /usb1 ici suffit pour rendre 10.55.0.2 joignable depuis
# 10.55.0.1, peu importe le canal physique sélectionné par l'hôte.
local retry=0
while [[ ! -d /sys/class/net/usb1 ]] && [[ $retry -lt 10 ]]; do
sleep 0.5
@ -294,9 +299,9 @@ gadget_start() {
done
if [[ -d /sys/class/net/usb1 ]]; then
log "Interface usb1 (ECM) créée"
log "Interface usb1 (deuxième fonction réseau du gadget) créée"
# Configurer l'IP sur usb1 uniquement (évite le routage asymétrique)
# Configurer l'IP sur usb1 uniquement (évite le routage asymétrique).
ip addr flush dev usb1 2>/dev/null || true
ip addr add "${OTG_NETWORK_DEV}/30" dev usb1
ip link set usb1 up

View File

@ -704,15 +704,12 @@ ln -sf /etc/systemd/system/secubox-serial-console.service \
# NOTE: fb-dashboard is DEPRECATED - use fallback-display instead
# The old dashboard is kept for reference but NOT enabled by default
# Network config for usb0
mkdir -p "$ROOT_MNT/etc/network/interfaces.d"
cat > "$ROOT_MNT/etc/network/interfaces.d/usb0" << 'EOF'
allow-hotplug usb0
iface usb0 inet static
address 10.55.0.2
netmask 255.255.255.252
gateway 10.55.0.1
EOF
# IP binding for the gadget's usb1 interface is handled by
# /usr/local/sbin/secubox-otg-gadget.sh (the same script that composes
# the configfs gadget). The legacy /etc/network/interfaces.d/usb0
# stanza was removed (closes #139) — it required `ifupdown` which the
# image never installed, so it never did anything except confuse
# diagnostics.
# USB network script (handles both usb0 and usb1 - ECM may create either)
mkdir -p "$ROOT_MNT/usr/local/bin"
@ -787,8 +784,9 @@ if ! id secubox &>/dev/null; then
echo "secubox:secubox2026" | chpasswd
fi
# Add to groups
usermod -aG video,input,gpio,i2c,spi,audio secubox 2>/dev/null || true
# Add to groups. `sudo` lets the kiosk user manually recover networking
# from the ACM serial console (e.g. when /dev/ttyACM0 is the only path in).
usermod -aG sudo,video,input,gpio,i2c,spi,audio secubox 2>/dev/null || true
# Enable lightdm and nginx
systemctl enable lightdm 2>/dev/null || true

View File

@ -1,5 +0,0 @@
allow-hotplug usb0
iface usb0 inet static
address 10.55.0.2
netmask 255.255.255.252
gateway 10.55.0.1

View File

@ -0,0 +1,95 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
#
# tests/scripts/test-mail-phase1-acceptance.sh
# Phase 1 rev. 2 acceptance — runs against the live board.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO="$(cd "$SCRIPT_DIR/../.." && pwd)"
# shellcheck source=/dev/null
source "$REPO/scripts/lib/test-helpers.sh"
HOST="${1:-root@192.168.1.200}"
HOST_IP="${HOST#*@}"
step() { echo; echo "[acceptance] $*"; }
fail() { echo "FAIL: $*" >&2; exit 1; }
step "1) Source-side: bash -n parses every controller cleanly"
for f in "$REPO"/packages/secubox-mail/sbin/{mailctl,mailserverctl,roundcubectl,mail-migrate-to-single-lxc.sh}; do
bash -n "$f" || fail "bash -n $f"
done
pass "controllers parse"
step "2) Pytest: 62 endpoints respond non-5xx"
( cd "$REPO" && python3 -m pytest packages/secubox-mail/api/tests/test_phase1_endpoints.py -q ) >/dev/null \
|| fail "pytest endpoint coverage"
pass "62 endpoints respond"
step "3) Board: canonical paths present (LXC + data volumes)"
ssh "$HOST" 'set -e
test -d /data/lxc/mail/rootfs
test -L /var/lib/lxc/mail
test -d /data/volumes/mail/vmail
' || fail "canonical paths missing on board"
pass "canonical paths exist"
step "4) Board: production users still present in /data/volumes/mail/vmail/secubox.in/"
ssh "$HOST" 'ls /data/volumes/mail/vmail/secubox.in/' > /tmp/phase1-users
for u in gk2 bat bourdon lemurien ragondin; do
grep -wq "$u" /tmp/phase1-users || fail "production user '$u' missing — data preservation invariant I13 violated"
done
pass "5 production users present (gk2, bat, bourdon, lemurien, ragondin)"
step "5) Board: host secubox-mail.service active"
ssh "$HOST" 'systemctl is-active secubox-mail' | grep -q "^active$" || fail "secubox-mail.service inactive"
pass "secubox-mail.service active"
step "6) Board: mailctl migrate-config is idempotent on already-migrated toml"
ssh "$HOST" '/usr/sbin/mailctl migrate-config 2>&1' | tail -3
pass "migrate-config ran (idempotent)"
step "7) Board: /etc/secubox/mail.toml has canonical keys + no live old keys"
ssh "$HOST" 'cat /etc/secubox/mail.toml' > /tmp/phase1-toml
grep -qE '^container *= *"mail"' /tmp/phase1-toml || fail "missing canonical 'container = \"mail\"'"
grep -qE '^lxc_ip *= *"10\.100\.0\.10"' /tmp/phase1-toml || fail "missing canonical 'lxc_ip = \"10.100.0.10\"'"
# Old keys should be either gone or commented as DEPRECATED
if grep -qE '^[^#]*mail_container *=' /tmp/phase1-toml; then
fail "legacy 'mail_container' key still active (not commented)"
fi
pass "toml uses canonical keys; legacy keys commented or removed"
step "8) Board: mailctl start brings the mail LXC up"
ssh "$HOST" 'mailctl start 2>&1' | tail -5
ssh "$HOST" 'lxc-info -n mail 2>&1' | grep -E "State:.*RUNNING" >/dev/null \
|| fail "mail LXC not RUNNING after mailctl start"
pass "mail LXC RUNNING"
step "9) Board: Postfix:25 + Dovecot:993 + HTTP:80 listening inside LXC"
ssh "$HOST" 'lxc-attach -n mail -- ss -tlnp 2>&1' > /tmp/phase1-ports
grep -q ':25 ' /tmp/phase1-ports || fail "postfix:25 not listening"
grep -q ':993 ' /tmp/phase1-ports || fail "dovecot:993 not listening"
grep -q ':80 ' /tmp/phase1-ports || fail "http:80 (Roundcube) not listening"
pass "postfix:25, dovecot:993, http:80 listening"
step "10) Board: Dovecot greets on IMAPS port"
out=$(ssh "$HOST" 'echo "0 LOGOUT" | timeout 5 openssl s_client -connect 10.100.0.10:993 -quiet 2>/dev/null | head -3' || true)
echo "$out" | grep -qi "Dovecot" || fail "no Dovecot greeting from 10.100.0.10:993"
pass "Dovecot IMAPS greeting received"
step "11) Board: Roundcube reachable through host proxy (WAF path)"
out=$(curl --silent --insecure --resolve "webmail.gk2.secubox.in:443:$HOST_IP" \
https://webmail.gk2.secubox.in/ 2>&1 || true)
echo "$out" | grep -qiE 'roundcube|webmail|login' \
|| fail "Roundcube did not respond through WAF path"
pass "Roundcube reachable via WAF (HAProxy → mitmproxy → LXC :80)"
step "12) Data preservation (gate 4 re-check after the LXC started)"
ssh "$HOST" 'ls /data/volumes/mail/vmail/secubox.in/' > /tmp/phase1-users-after
diff /tmp/phase1-users /tmp/phase1-users-after || fail "user list changed after LXC start"
pass "5 production users still byte-identical after LXC start"
echo
pass "PHASE 1 ACCEPTANCE: all 12 gates green"