Skip to content

[Sprint 5] Setup Wizard#6

Merged
uzyn merged 3 commits into
mainfrom
sprint-5-setup-wizard
Apr 9, 2026
Merged

[Sprint 5] Setup Wizard#6
uzyn merged 3 commits into
mainfrom
sprint-5-setup-wizard

Conversation

@uzyn

@uzyn uzyn commented Apr 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Implements the full setup wizard (aimx setup <domain>) with preflight checks, OpenSMTPD configuration, DNS guidance + verification, and setup finalization
  • Implements aimx preflight as a standalone command for running port 25 and PTR checks without proceeding to full setup
  • All system operations (apt install, service restart, file writing to /etc/) and network operations (port 25 checks, DNS lookups) are behind traits for testability — unit tests use mocks, no real system operations in CI

Stories

S5.1 — Preflight Checks

  • Outbound port 25 check (connects to gmail-smtp-in.l.google.com:25)
  • Inbound port 25 check (via check.aimx.email probe service)
  • PTR record check (non-blocking warning if not set)
  • Port 25 blocked stops setup with compatible VPS provider list
  • aimx preflight runs checks standalone
  • Unit tests for pass/fail/timeout paths using mock NetworkOps trait

S5.2 — OpenSMTPD Configuration

  • Installs OpenSMTPD via apt if not present
  • Generates self-signed TLS cert in /etc/ssl/aimx/
  • Writes smtpd.conf with TLS, inbound MDA delivery, outbound relay
  • Backs up existing smtpd.conf before overwriting
  • Restarts OpenSMTPD after configuration
  • Unit tests for smtpd.conf content generation and mock system operations

S5.3 — DNS Guidance + Verification

  • Displays all required DNS records: MX, A, SPF, DKIM, DMARC, PTR
  • Records include actual server IP and DKIM public key values
  • Pauses for user to confirm DNS records are added
  • Verifies each record via DNS lookup after confirmation
  • Shows which records are wrong/missing with guidance
  • Unit tests for record formatting and verification logic (pass/fail/missing)

S5.4 — Setup Finalization

  • Creates default catchall mailbox
  • Generates DKIM keypair if not present
  • Creates data directory with config.yaml
  • Displays MCP configuration snippet for Claude Code
  • Displays Gmail whitelist instructions
  • Idempotent — running again does not break existing config or overwrite keys
  • Unit tests for finalization including idempotency and existing mailbox preservation

Test plan

  • 188 unit tests pass (44 new in setup module)
  • 32 integration tests pass (3 new for CLI arg parsing)
  • cargo clippy -- -D warnings clean
  • cargo fmt -- --check clean
  • All trait-based abstractions allow testing without real system/network operations

…uidance, and finalization

- Add setup module with trait-based abstractions for system and network operations
- Preflight checks: outbound/inbound port 25, PTR record, compatible providers list
- OpenSMTPD configuration: apt install, self-signed TLS, smtpd.conf generation, backup
- DNS guidance: display all records (MX, A, SPF, DKIM, DMARC, PTR), verify via lookup
- Setup finalization: catchall mailbox, DKIM keygen, data dir, MCP snippet, Gmail whitelist
- Wire up `aimx setup <domain>` and `aimx preflight` CLI commands
- 44 unit tests covering all logic paths with mock traits
- 3 integration tests for CLI argument parsing

@uzyn uzyn left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sprint 5 Review: Setup Wizard

Sprint Goal Assessment

Goal: Replace all manual VPS setup with a single aimx setup <domain> command that handles everything from preflight checks to DNS verification.

Assessment: The sprint goal is substantially met. The implementation delivers a complete setup wizard flow: preflight checks, OpenSMTPD configuration, DNS guidance with interactive verification, and setup finalization with DKIM keygen, catchall mailbox, and MCP snippet. The aimx preflight standalone command is also implemented. The code is well-structured with SystemOps and NetworkOps traits for full testability.

Acceptance Criteria Checklist

S5.1 -- Preflight Checks

Criterion Status
Outbound port 25 check connects to well-known MX and reports pass/fail MET
Inbound port 25 check requests probe from check.aimx.email and reports pass/fail MET
PTR record check warns (non-blocking) if not set, with instructions MET
Port 25 blocked -> setup stops with error listing compatible VPS providers MET
aimx preflight runs checks standalone without proceeding to setup MET
Unit tests for each check result path using mockable network traits MET (8 tests)

S5.2 -- OpenSMTPD Configuration

Criterion Status
Setup installs OpenSMTPD if not present (via apt) MET
Self-signed TLS cert generated and placed in /etc/ssl/aimx/ MET
smtpd.conf written with TLS, inbound delivery, outbound relay MET
OpenSMTPD restarted successfully after configuration MET
Existing config is backed up before overwriting MET
Unit test: generated smtpd.conf content correct MET (2 tests)
Unit test: TLS cert generation PARTIAL -- mock returns Ok(()) but does not verify cert validity

S5.3 -- DNS Guidance + Verification

Criterion Status
Setup displays all required DNS records: MX, A, SPF, DKIM, DMARC, PTR MET
Records include actual values (server IP, DKIM public key) MET
Setup pauses and waits for user to confirm DNS records added MET
After confirmation, verifies each record via DNS lookup MET
Failed verification shows which records are wrong/missing MET
Unit test: DNS record display formatting MET
Unit test: verification logic handles pass/fail/missing MET (12 tests)

S5.4 -- Setup Finalization

Criterion Status
Default catchall mailbox created MET
DKIM keypair generated (if not already present) MET
Data directory created with correct permissions MET
MCP configuration snippet displayed MET
Gmail whitelist instructions displayed MET
Setup is idempotent MET (3 tests)

Bugs Found

BUG 1 (Medium): OpenSMTPD restarted before config.yaml exists

In run_setup() (line 700), configure_opensmtpd() is called which restarts OpenSMTPD with the new smtpd.conf pointing MDA delivery to aimx ingest %{rcpt}. However, finalize_setup() which creates config.yaml, the catchall mailbox, and DKIM keys is called AFTER on line 714. If any email arrives in this window, aimx ingest will fail because config.yaml does not yet exist.

Fix: Move finalize_setup() (and the data_dir/config_path/dkim_selector setup) to BEFORE configure_opensmtpd(), so that by the time OpenSMTPD starts accepting mail, the data directory and config are ready.

BUG 2 (Low): run_preflight_command uses process::exit(1) instead of returning an error

run_preflight_command() at line 743 calls std::process::exit(1) directly when checks fail, bypassing normal error propagation and the error-handling in main(). The run_setup function correctly returns Err(...) for the same scenario (line 697). This is inconsistent and could skip cleanup/destructors.

Fix: Return Err(...) instead of calling process::exit(1), letting main() handle the exit code.

Security Observations (Non-blocking)

  1. No domain input validation: The domain argument from the CLI is used directly in smtpd.conf generation without validation. A domain containing double quotes or newlines could inject arbitrary OpenSMTPD config directives. Since this is a root-run CLI tool and the domain is self-provided, the attack surface is self-harm only. Still, basic domain format validation (RFC 1035 characters: alphanumeric, hyphens, dots) would be good defensive coding. Not a blocker.

  2. Backup overwrite on repeated setup: Running aimx setup twice overwrites /etc/smtpd.conf.bak with the first run's generated config, losing the original pre-aimx config. Acceptable for now but worth documenting.

Test Coverage

Strong coverage areas:

  • 44 unit tests covering all public functions
  • Mock-based testing via SystemOps and NetworkOps traits -- excellent design
  • Idempotency tests for finalization
  • 3 new integration tests for CLI argument parsing
  • All 188 unit tests + 32 integration tests pass
  • cargo clippy -- -D warnings clean
  • cargo fmt -- --check clean

Gaps (non-blocking):

  • No test for the full run_setup flow (would require both mock traits and stdin simulation)
  • TLS cert generation test only verifies the mock returns Ok, not that the openssl command produces valid output (acceptable since it's behind a trait)
  • No test for run_preflight_command specifically

Alignment with PRD

The implementation aligns well with PRD requirements:

  • FR-1 (preflight checks): Implemented -- outbound port 25 via gmail-smtp-in, inbound via check.aimx.email
  • FR-2 (port 25 blocked error): Implemented -- lists compatible providers
  • FR-3 (PTR warning): Implemented -- non-blocking warning
  • FR-4 (OpenSMTPD config): Implemented -- TLS, MDA delivery, outbound relay
  • FR-5 (DKIM keygen): Integrated into setup finalization
  • FR-6 (DNS records display): Implemented -- all 6 record types with actual values
  • FR-7 (DNS verification): Implemented -- per-record verification
  • FR-9 (catchall mailbox): Implemented
  • FR-10 (MCP config snippet): Implemented

The aimx preflight standalone command (FR-42) is also implemented.

Summary

Verdict: CHANGES REQUESTED (posted as comment due to self-owned PR)

The implementation is solid and well-tested. The trait-based abstraction for system and network operations is excellent engineering. Two bugs were found:

  1. OpenSMTPD restart ordering (medium) -- OpenSMTPD starts accepting mail before config.yaml exists. Easy fix: reorder the calls in run_setup.
  2. process::exit in preflight command (low) -- inconsistent error handling. Easy fix: return Err instead.

Both are straightforward fixes that should not require significant rework. Fix the ordering bug and this is ready to merge.

- Reorder run_setup to call finalize_setup before configure_opensmtpd,
  ensuring config.yaml and data directory exist before OpenSMTPD starts
  accepting mail
- Replace process::exit(1) in run_preflight_command with Err return,
  consistent with run_setup error handling
- Add domain input validation (RFC 1035 characters) to prevent config
  injection via malformed domain names
- Add tests for domain validation and preflight command error path
@uzyn

uzyn commented Apr 9, 2026

Copy link
Copy Markdown
Owner Author

Review Feedback Addressed

Fixed

  1. Setup ordering (Blocker): Reordered run_setup() to call finalize_setup() before configure_opensmtpd(). Config.yaml, the data directory, catchall mailbox, and DKIM keys are now all in place before OpenSMTPD is restarted and can start accepting mail.

  2. run_preflight_command uses process::exit(1) (Non-blocker): Replaced with return Err(...) so error propagation is consistent with run_setup and main() handles the exit code. Added tests for both success and failure paths.

  3. No domain input validation (Non-blocker): Added validate_domain() that checks RFC 1035 label rules (alphanumeric + hyphens, no leading/trailing hyphens, 2+ labels, length limits). Called at the top of run_setup() before any side effects. Added 5 test cases covering valid domains, empty input, single labels, special characters, and leading/trailing hyphens.

Intentionally Left As-Is

  1. Backup overwrite on repeated setup: Running setup twice still overwrites smtpd.conf.bak with the previous run's generated config. This is acceptable for now -- the backup's purpose is to preserve the pre-aimx config on first run, and users running setup repeatedly are already aware they're in aimx-managed territory. A timestamped backup scheme would add complexity without meaningful safety benefit at this stage.

Test Results

  • 195 unit tests pass (7 new)
  • 32 integration tests pass
  • cargo clippy -- -D warnings clean
  • cargo fmt -- --check clean

@uzyn uzyn left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-Review: Sprint 5 Setup Wizard (Round 2)

Previously Flagged Issues

1. Setup ordering (Blocker) -- RESOLVED

Confirmed: finalize_setup() is now called before configure_opensmtpd() in run_setup() (lines 740-742). Config, data dir, catchall mailbox, and DKIM keys are all in place before OpenSMTPD restarts.

2. process::exit(1) replaced with return Err(...) -- RESOLVED

Confirmed: No process::exit calls remain in setup.rs. run_preflight_command() returns Err(...) on failure. Tests cover both success and failure paths.

3. Domain input validation -- RESOLVED

Confirmed: validate_domain() checks empty, max length (253), label length (63), leading/trailing hyphens, allowed characters (alphanumeric + hyphen), and minimum 2 labels. Called at the top of run_setup() before any side effects. Five test cases cover the key cases.

4. Backup overwrite on repeated setup -- ACCEPTED (deferred)

Agree with the reasoning. The backup's purpose is preserving the pre-aimx config, and repeat-setup users are in managed territory. Not worth the complexity now.

New Issue Found

Bug: Catchall address not updated on domain change (Blocker)

In finalize_setup(), when the domain changes (lines 627-629), config.domain is updated and saved, but existing mailbox addresses are NOT updated. Specifically, if catchall already exists with address: "*@old.example.com" and setup is re-run with new.example.com, the config ends up with:

domain: "new.example.com"
mailboxes:
  catchall:
    address: "*@old.example.com"   # stale!

The address field is used for matching incoming mail and as the sender address (see mcp.rs:248, mailbox.rs:51), so this creates a real inconsistency.

Fix: When updating the domain on lines 627-629, also iterate through mailboxes and update any addresses that end with @{old_domain} to use the new domain. The existing test finalize_updates_domain_if_changed should also assert that catchall.address contains the new domain.

This is an edge case (re-running setup with a different domain) but the code explicitly handles the domain-change path, so the address should be updated too.

Verdict: CHANGES REQUESTED

The three previously flagged issues are all properly resolved, and issue 4 is reasonably deferred. However, there is one new bug in the domain-change path of finalize_setup(). Requesting one more fix round.

When re-running setup with a different domain, mailbox addresses (e.g.,
catchall's "*@old.example.com") were not updated to reflect the new domain,
creating stale config. Now iterates through all mailboxes and updates
addresses whose domain portion matches the old domain.

Updated finalize_updates_domain_if_changed test to assert catchall address.
@uzyn

uzyn commented Apr 9, 2026

Copy link
Copy Markdown
Owner Author

Review Feedback Addressed (Round 3)

Fixed

  1. Bug: Catchall address not updated on domain change (Blocker): When re-running finalize_setup() with a different domain, config.domain was updated but mailbox addresses (e.g., catchall.address: "*@old.example.com") were left stale. Now, when the domain changes, all mailbox addresses whose domain portion matches the old domain are updated to the new domain. The finalize_updates_domain_if_changed test now also asserts that catchall.address reflects the new domain.

Test Results

  • 195 unit tests pass
  • 32 integration tests pass
  • cargo clippy -- -D warnings clean
  • cargo fmt -- --check clean

@uzyn uzyn left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review (Round 3) -- APPROVED

All previously flagged issues have been verified as resolved:

Round 1 blockers (verified resolved)

  1. Setup ordering -- finalize_setup() is now called before configure_opensmtpd() at lines 750-752 of setup.rs. Config, data directory, catchall mailbox, and DKIM keys are all in place before OpenSMTPD restarts.

Round 1 non-blockers (verified resolved)

  1. process::exit replaced with Err -- run_preflight_command returns Err(...) on failure, propagated through main(). Tests cover both paths.
  2. Domain validation -- validate_domain() enforces RFC 1035 label rules and is called at the top of run_setup() before any side effects. 5 test cases cover valid domains, empty input, single labels, special characters, and hyphen edge cases.

Round 1 non-blockers (deferred, accepted)

  1. Backup overwrite on repeated setup -- Accepted as-is per prior discussion.

Round 2 blockers (verified resolved)

  1. Mailbox addresses not updated on domain change -- Fixed in commit 4896db7b. When the domain changes in finalize_setup(), all mailbox addresses whose domain portion matches the old domain are rewritten to the new domain (lines 630-638). The finalize_updates_domain_if_changed test now explicitly asserts that catchall.address reflects *@new.example.com.

Verification

  • 195 unit tests pass (including domain-change mailbox address assertion)
  • 32 integration tests pass
  • cargo clippy -- -D warnings clean
  • No new issues found

Recommended merge commit message

feat: setup wizard with preflight checks, OpenSMTPD config, and DNS guidance

Add `aimx setup <domain>` command that runs port 25 reachability and PTR
preflight checks, generates config.yaml with a catchall mailbox, creates
DKIM keys, writes and restarts OpenSMTPD with TLS, and displays required
DNS records (A, MX, SPF, DKIM, DMARC, PTR) with inline verification.

Add `aimx preflight` as a standalone check. Domain input is validated
against RFC 1035 label rules. Re-running setup with a different domain
correctly updates all mailbox addresses. Includes MCP config snippet
and Gmail whitelist instructions in post-setup output.

@uzyn uzyn merged commit 3a2746c into main Apr 9, 2026
2 checks passed
@uzyn uzyn deleted the sprint-5-setup-wizard branch April 10, 2026 00:32
uzyn added a commit that referenced this pull request Apr 21, 2026
Add setup wizard with preflight checks, OpenSMTPD config, and DNS guidance

Add `aimx setup <domain>` command that runs port 25 reachability and PTR
preflight checks, generates config.yaml with a catchall mailbox, creates
DKIM keys, writes and restarts OpenSMTPD with TLS, and displays required
DNS records (A, MX, SPF, DKIM, DMARC, PTR) with inline verification.

Add `aimx preflight` as a standalone check. Domain input is validated
against RFC 1035 label rules. Re-running setup with a different domain
correctly updates all mailbox addresses. Includes MCP config snippet
and Gmail whitelist instructions in post-setup output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant