"""Poste.io initial setup scenario. Automates the first-time setup for a fresh Poste.io mail server installation. This scenario: 1. Navigates to the Poste.io admin setup page 2. Configures the mailserver hostname 3. Creates the admin email account with a generated password 4. Returns the generated credentials for secure storage """ import secrets import string from typing import Any from playwright.async_api import Page from app.playwright_scenarios import register_scenario from app.playwright_scenarios.base import BaseScenario, ScenarioOptions, ScenarioResult def generate_secure_password(length: int = 24) -> str: """Generate a cryptographically secure password. Args: length: Password length (default: 24) Returns: A secure random password with mixed characters """ # Use a mix of letters, digits, and safe special characters alphabet = string.ascii_letters + string.digits + "!@#$%^&*" # Ensure at least one of each type password = [ secrets.choice(string.ascii_lowercase), secrets.choice(string.ascii_uppercase), secrets.choice(string.digits), secrets.choice("!@#$%^&*"), ] # Fill the rest randomly password.extend(secrets.choice(alphabet) for _ in range(length - 4)) # Shuffle to avoid predictable positions password_list = list(password) secrets.SystemRandom().shuffle(password_list) return "".join(password_list) @register_scenario class PosteInitialSetup(BaseScenario): """Automate Poste.io first-time setup wizard. This scenario handles the initial server configuration when Poste.io is freshly installed. It configures the mailserver hostname and creates the administrator email account. Required inputs: base_url: The Poste.io instance URL (e.g., https://mail.example.com) admin_email: Admin email address (e.g., admin@example.com) Optional inputs: admin_password: Password for admin account (auto-generated if not provided) mailserver_hostname: Override mailserver hostname (defaults to URL hostname) Result data: setup_completed: Whether initial setup was completed admin_email: The configured admin email address admin_password: The password (generated or provided) - STORE SECURELY mailserver_hostname: The configured hostname already_configured: True if Poste was already set up """ @property def name(self) -> str: return "poste_initial_setup" @property def required_inputs(self) -> list[str]: return ["base_url", "admin_email"] @property def optional_inputs(self) -> list[str]: return ["admin_password", "mailserver_hostname"] @property def description(self) -> str: return "Automate Poste.io first-time mail server setup" async def execute( self, page: Page, inputs: dict[str, Any], options: ScenarioOptions, ) -> ScenarioResult: """Execute the Poste.io initial setup. Args: page: Playwright Page object inputs: Scenario inputs (base_url, admin_email, optional password) options: Scenario options Returns: ScenarioResult with setup status and credentials """ base_url = inputs["base_url"].rstrip("/") admin_email = inputs["admin_email"] # Generate password if not provided admin_password = inputs.get("admin_password") or generate_secure_password() # Extract hostname from URL if not provided from urllib.parse import urlparse parsed_url = urlparse(base_url) mailserver_hostname = inputs.get("mailserver_hostname") or parsed_url.netloc screenshots = [] result_data = { "setup_completed": False, "admin_email": admin_email, "admin_password": admin_password, # Return for secure storage "mailserver_hostname": mailserver_hostname, "already_configured": False, } try: # Navigate to Poste.io await page.goto(base_url, wait_until="networkidle") current_url = page.url # Check if we're on the setup page if "/admin/install/server" not in current_url: # Check if redirected to login (already configured) if "/admin/login" in current_url or "/webmail" in current_url: result_data["already_configured"] = True result_data["setup_completed"] = True return ScenarioResult( success=True, data=result_data, screenshots=screenshots, error=None, ) # Try navigating directly to setup page await page.goto(f"{base_url}/admin/install/server", wait_until="networkidle") # If still not on setup, it's already configured if "/admin/install/server" not in page.url: result_data["already_configured"] = True result_data["setup_completed"] = True return ScenarioResult( success=True, data=result_data, screenshots=screenshots, error=None, ) # We're on the setup page - configure the mail server # Wait for the hostname input to be visible hostname_input = page.locator('input[placeholder*="mail.example.com"]') await hostname_input.wait_for(state="visible", timeout=10000) # Clear and fill hostname (may be pre-filled) await hostname_input.clear() await hostname_input.fill(mailserver_hostname) # Fill admin email admin_email_input = page.locator('input[placeholder*="admin@example.com"]') await admin_email_input.wait_for(state="visible", timeout=5000) await admin_email_input.fill(admin_email) # Fill password password_input = page.locator('input[type="password"], input[placeholder*="Password"]').last await password_input.wait_for(state="visible", timeout=5000) await password_input.fill(admin_password) # Take screenshot before submitting if requested if options.screenshot_on_success and options.artifacts_dir: pre_submit_path = options.artifacts_dir / "poste_pre_submit.png" await page.screenshot(path=str(pre_submit_path)) screenshots.append(str(pre_submit_path)) # Click Submit button submit_button = page.locator('button:has-text("Submit")') await submit_button.click() # Wait for setup to complete - should redirect away from install page try: await page.wait_for_url( lambda url: "/admin/install" not in url, timeout=60000, # 60 seconds for setup ) result_data["setup_completed"] = True except Exception: # Check if there's an error message error_el = page.locator('.error, .alert-danger, [class*="error"]') if await error_el.count() > 0: error_text = await error_el.first.text_content() return ScenarioResult( success=False, data=result_data, screenshots=screenshots, error=f"Setup failed: {error_text}", ) # Still on page but no error - might have succeeded result_data["setup_completed"] = True # Take final screenshot if options.screenshot_on_success and options.artifacts_dir: final_path = options.artifacts_dir / "poste_setup_complete.png" await page.screenshot(path=str(final_path)) screenshots.append(str(final_path)) return ScenarioResult( success=result_data["setup_completed"], data=result_data, screenshots=screenshots, error=None, ) except Exception as e: # Take error screenshot if options.screenshot_on_failure and options.artifacts_dir: error_path = options.artifacts_dir / "poste_setup_error.png" await page.screenshot(path=str(error_path)) screenshots.append(str(error_path)) return ScenarioResult( success=False, data=result_data, screenshots=screenshots, error=f"Poste.io setup failed: {str(e)}", )