diff --git a/README.md b/README.md index e2ad6b3..5cfe258 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ cd openmailserver python3 -m venv .venv .venv/bin/python -m pip install -e ".[dev]" .venv/bin/openmailserver preflight -.venv/bin/openmailserver install +.venv/bin/openmailserver install --domain yourdomain.com --hostname mail.yourdomain.com .venv/bin/openmailserver mox-quickstart docker compose up -d .venv/bin/openmailserver doctor @@ -52,7 +52,7 @@ After the stack is up: .venv/bin/openmailserver create-mailbox agent yourdomain.com curl http://127.0.0.1:8787/health .venv/bin/openmailserver smoke-test -.venv/bin/openmailserver plan-dns +.venv/bin/openmailserver plan-dns --public-ip ``` ## Custom Port Binds diff --git a/docs/dns.md b/docs/dns.md index a18e570..12e37ba 100644 --- a/docs/dns.md +++ b/docs/dns.md @@ -1,6 +1,7 @@ # DNS Setup -`openmailserver plan-dns` prints the records required for direct-to-MX delivery. +`openmailserver plan-dns --public-ip ` prints the records +required for direct-to-MX delivery. The expected flow is that you configure Open Mailserver for the real domain first, then use this output to finish DNS before expecting internet mail to work diff --git a/docs/install.md b/docs/install.md index a177e72..fca652c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -64,7 +64,7 @@ cd openmailserver python3 -m venv .venv .venv/bin/python -m pip install -e ".[dev]" .venv/bin/openmailserver preflight -.venv/bin/openmailserver install +.venv/bin/openmailserver install --domain yourdomain.com --hostname mail.yourdomain.com .venv/bin/openmailserver mox-quickstart docker compose up -d .venv/bin/openmailserver doctor @@ -80,14 +80,15 @@ loopback, add `--api-bind` too: ```bash .venv/bin/openmailserver install \ + --domain yourdomain.com \ + --hostname mail.yourdomain.com \ --api-bind 127.0.0.1:9787 \ --mox-http-bind 127.0.0.1:8080 \ --mox-https-bind 127.0.0.1:8443 ``` -Review the generated `.env` and set `OPENMAILSERVER_PRIMARY_DOMAIN`, -`OPENMAILSERVER_CANONICAL_HOSTNAME`, and related values for the real domain you -intend to host. +The install step writes the provided domain and hostname into the generated +`.env`. Review the rest of the generated values before you continue. The install step writes: @@ -187,7 +188,7 @@ mailbox can also be accessed with: Once the stack is running, use: ```bash -.venv/bin/openmailserver plan-dns +.venv/bin/openmailserver plan-dns --public-ip ``` That output is the DNS checklist for direct-to-MX delivery on the public @@ -213,7 +214,6 @@ Most important values: - `OPENMAILSERVER_SMTP_HOST` - `OPENMAILSERVER_CANONICAL_HOSTNAME` - `OPENMAILSERVER_PRIMARY_DOMAIN` -- `OPENMAILSERVER_PUBLIC_IP` - `OPENMAILSERVER_MOX_HTTP_BIND` - `OPENMAILSERVER_MOX_HTTPS_BIND` - `OPENMAILSERVER_MOX_ADMIN_ACCOUNT` @@ -221,3 +221,7 @@ Most important values: - `OPENMAILSERVER_ADMIN_API_KEY` - `OPENMAILSERVER_BACKUP_ENCRYPTION_KEY` - `OPENMAILSERVER_MOX_IMAGE` + +`openmailserver plan-dns` now takes the server IP explicitly with +`--public-ip`, so you do not need to set `OPENMAILSERVER_PUBLIC_IP` during +install just to generate the DNS plan. diff --git a/docs/operations.md b/docs/operations.md index 41039e1..6b514be 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -3,8 +3,8 @@ ## CLI - `openmailserver preflight` -- `openmailserver install` -- `openmailserver plan-dns` +- `openmailserver install --domain --hostname ` +- `openmailserver plan-dns --public-ip ` - `openmailserver doctor` - `openmailserver create-mailbox ` - `openmailserver smoke-test` @@ -29,10 +29,11 @@ Open Mailserver is designed for the user to supply the real domain up front, then verify the stack on the current machine while DNS is still being completed. -- configure the real domain and canonical mail hostname in `.env` +- set the real domain and canonical mail hostname during `openmailserver install` - confirm `docker compose up`, `curl /health`, mailbox creation, and `openmailserver smoke-test` -- use `openmailserver plan-dns` to finish the internet-facing setup +- use `openmailserver plan-dns --public-ip ` to finish the + internet-facing setup ## Direct Delivery Requirements diff --git a/src/openmailserver/cli.py b/src/openmailserver/cli.py index 7c9e8ae..d757ff3 100644 --- a/src/openmailserver/cli.py +++ b/src/openmailserver/cli.py @@ -151,16 +151,26 @@ def preflight() -> None: def _run_install( *, + domain: str, + hostname: str, api_bind: str | None = None, mox_http_bind: str | None = None, mox_https_bind: str | None = None, ) -> None: - settings = _install_settings_with_overrides( + base_settings = _install_settings_with_overrides( get_settings(), api_bind=api_bind, mox_http_bind=mox_http_bind, mox_https_bind=mox_https_bind, ) + settings = base_settings.model_copy( + update={ + "primary_domain": domain, + "canonical_hostname": hostname, + "imap_host": None, + "mox_admin_address": f"{base_settings.mox_admin_account}@{domain}", + } + ) settings.ensure_directories() create_all() admin_key = settings.admin_api_key or _bootstrap_admin_key() @@ -195,6 +205,10 @@ def _run_install( @app.command() def install( + domain: str = typer.Option(..., "--domain", help="Primary domain to host mail for."), + hostname: str = typer.Option( + ..., "--hostname", help="Canonical mail hostname, for example mail.example.com." + ), api_bind: str | None = typer.Option( None, help="Docker host bind for the API service, for example 8787 or 127.0.0.1:8787.", @@ -210,6 +224,8 @@ def install( ) -> None: """Generate local config, container runtime directories, and install metadata.""" _run_install( + domain=domain, + hostname=hostname, api_bind=api_bind, mox_http_bind=mox_http_bind, mox_https_bind=mox_https_bind, @@ -240,7 +256,11 @@ def mox_quickstart() -> None: @app.command("plan-dns") -def plan_dns() -> None: +def plan_dns( + public_ip: str = typer.Option( + ..., "--public-ip", help="Public IP address for the mail host." + ), +) -> None: """Print the DNS records required for direct-to-MX setup.""" settings = get_settings() typer.echo( @@ -248,7 +268,7 @@ def plan_dns() -> None: { "hostname": settings.canonical_hostname, "domain": settings.primary_domain, - "records": build_dns_plan(), + "records": build_dns_plan(public_ip=public_ip), }, indent=2, ) @@ -395,9 +415,32 @@ def restore(path: str) -> None: @app.command() -def bootstrap() -> None: +def bootstrap( + domain: str = typer.Option(..., "--domain", help="Primary domain to host mail for."), + hostname: str = typer.Option( + ..., "--hostname", help="Canonical mail hostname, for example mail.example.com." + ), + api_bind: str | None = typer.Option( + None, + help="Docker host bind for the API service, for example 8787 or 127.0.0.1:8787.", + ), + mox_http_bind: str | None = typer.Option( + None, + help="Docker host bind for mox HTTP, for example 80 or 127.0.0.1:8080.", + ), + mox_https_bind: str | None = typer.Option( + None, + help="Docker host bind for mox HTTPS, for example 443 or 127.0.0.1:8443.", + ), +) -> None: """Convenience wrapper for install -> doctor.""" - _run_install() + _run_install( + domain=domain, + hostname=hostname, + api_bind=api_bind, + mox_http_bind=mox_http_bind, + mox_https_bind=mox_https_bind, + ) doctor() diff --git a/src/openmailserver/services/dns_service.py b/src/openmailserver/services/dns_service.py index 6e4847d..fd2f704 100644 --- a/src/openmailserver/services/dns_service.py +++ b/src/openmailserver/services/dns_service.py @@ -3,11 +3,12 @@ from openmailserver.config import get_settings -def build_dns_plan() -> list[dict[str, str]]: +def build_dns_plan(*, public_ip: str | None = None) -> list[dict[str, str]]: settings = get_settings() selector = "openmail" + target_ip = public_ip or settings.public_ip return [ - {"type": "A", "host": settings.canonical_hostname, "value": settings.public_ip}, + {"type": "A", "host": settings.canonical_hostname, "value": target_ip}, { "type": "MX", "host": settings.primary_domain, @@ -34,5 +35,5 @@ def build_dns_plan() -> list[dict[str, str]]: "host": f"_dmarc.{settings.primary_domain}", "value": f'"v=DMARC1; p=none; rua=mailto:dmarc@{settings.primary_domain}"', }, - {"type": "PTR", "host": settings.public_ip, "value": settings.canonical_hostname}, + {"type": "PTR", "host": target_ip, "value": settings.canonical_hostname}, ] diff --git a/src/openmailserver/services/runtime_setup.py b/src/openmailserver/services/runtime_setup.py index c8bc7c3..423bcae 100644 --- a/src/openmailserver/services/runtime_setup.py +++ b/src/openmailserver/services/runtime_setup.py @@ -58,8 +58,8 @@ def render_runtime_bundle(settings: Settings, repo_root: Path) -> dict[str, str] ## Quickstart - 1. Review `.env` and set `OPENMAILSERVER_PRIMARY_DOMAIN` plus - `OPENMAILSERVER_CANONICAL_HOSTNAME` for the real domain you want to host. + 1. Review `.env` and confirm the install-time domain and hostname values + for the real domain you want to host. 2. Generate the `mox` config files: `{{ quickstart_command }}` diff --git a/tests/test_cli.py b/tests/test_cli.py index a89ebbf..196b32a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -11,8 +11,15 @@ runner = CliRunner() -def test_install_command_writes_runtime(): - result = runner.invoke(app, ["install"]) +def test_install_command_writes_runtime(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + cli.get_settings.cache_clear() + + result = runner.invoke( + app, + ["install", "--domain", "example.test", "--hostname", "mail.example.test"], + ) + assert result.exit_code == 0 assert "admin_api_key" in result.stdout assert "published_ports" in result.stdout @@ -20,6 +27,9 @@ def test_install_command_writes_runtime(): assert "container-mox" in result.stdout assert "quickstart_command" in result.stdout assert "docker compose up -d" in result.stdout + env_contents = (tmp_path / ".env").read_text(encoding="utf-8") + assert "OPENMAILSERVER_PRIMARY_DOMAIN=example.test" in env_contents + assert "OPENMAILSERVER_CANONICAL_HOSTNAME=mail.example.test" in env_contents def test_install_command_writes_bind_overrides_in_env(): @@ -28,6 +38,10 @@ def test_install_command_writes_bind_overrides_in_env(): app, [ "install", + "--domain", + "example.test", + "--hostname", + "mail.example.test", "--api-bind", "127.0.0.1:9787", "--mox-http-bind", @@ -46,7 +60,10 @@ def test_install_command_writes_bind_overrides_in_env(): def test_bootstrap_command_runs_install_and_doctor(): with runner.isolated_filesystem(): - result = runner.invoke(app, ["bootstrap"]) + result = runner.invoke( + app, + ["bootstrap", "--domain", "example.test", "--hostname", "mail.example.test"], + ) assert result.exit_code == 0 assert "admin_api_key" in result.stdout assert "published_ports" in result.stdout @@ -55,6 +72,8 @@ def test_bootstrap_command_runs_install_and_doctor(): assert "OPENMAILSERVER_API_BIND=8787" in env_text assert "OPENMAILSERVER_MOX_HTTP_BIND=80" in env_text assert "OPENMAILSERVER_MOX_HTTPS_BIND=443" in env_text + assert "OPENMAILSERVER_PRIMARY_DOMAIN=example.test" in env_text + assert "OPENMAILSERVER_CANONICAL_HOSTNAME=mail.example.test" in env_text def test_install_settings_with_overrides_no_overrides(): @@ -85,9 +104,19 @@ def test_install_settings_with_overrides_all(): def test_plan_dns_command_outputs_records(): - result = runner.invoke(app, ["plan-dns"]) + cli.get_settings.cache_clear() + result = runner.invoke(app, ["plan-dns", "--public-ip", "198.51.100.24"]) assert result.exit_code == 0 assert "MX" in result.stdout + assert "198.51.100.24" in result.stdout + + +def test_plan_dns_command_requires_public_ip(): + cli.get_settings.cache_clear() + result = runner.invoke(app, ["plan-dns"]) + + assert result.exit_code == 2 + assert "Usage:" in result.output def test_create_mailbox_delegates_to_api_container(monkeypatch):