Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 <server-public-ip>
```

## Custom Port Binds
Expand Down
3 changes: 2 additions & 1 deletion docs/dns.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# DNS Setup

`openmailserver plan-dns` prints the records required for direct-to-MX delivery.
`openmailserver plan-dns --public-ip <server-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
Expand Down
16 changes: 10 additions & 6 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand Down Expand Up @@ -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 <server-public-ip>
```

That output is the DNS checklist for direct-to-MX delivery on the public
Expand All @@ -213,11 +214,14 @@ 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`
- `OPENMAILSERVER_MOX_ADMIN_ADDRESS`
- `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.
9 changes: 5 additions & 4 deletions docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
## CLI

- `openmailserver preflight`
- `openmailserver install`
- `openmailserver plan-dns`
- `openmailserver install --domain <domain> --hostname <mail-hostname>`
- `openmailserver plan-dns --public-ip <server-public-ip>`
- `openmailserver doctor`
- `openmailserver create-mailbox <local-part> <domain>`
- `openmailserver smoke-test`
Expand All @@ -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 <server-public-ip>` to finish the
internet-facing setup

## Direct Delivery Requirements

Expand Down
53 changes: 48 additions & 5 deletions src/openmailserver/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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.",
Expand All @@ -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,
Expand Down Expand Up @@ -240,15 +256,19 @@ 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(
json.dumps(
{
"hostname": settings.canonical_hostname,
"domain": settings.primary_domain,
"records": build_dns_plan(),
"records": build_dns_plan(public_ip=public_ip),
},
indent=2,
)
Expand Down Expand Up @@ -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()


Expand Down
7 changes: 4 additions & 3 deletions src/openmailserver/services/dns_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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},
]
4 changes: 2 additions & 2 deletions src/openmailserver/services/runtime_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}`
Expand Down
37 changes: 33 additions & 4 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,25 @@
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
assert "runtime_files" in result.stdout
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():
Expand All @@ -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",
Expand All @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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):
Expand Down
Loading