'']); $ctx = DirectAdminContext::fromServer([ 'USERNAME' => 'mxtest', 'HTTP_HOST' => 'https://h4.domena.pl:2222', 'SERVER_NAME' => 'h4.domena.pl', 'REMOTE_ADDR' => '198.51.100.10', 'REQUEST_METHOD' => 'GET', ], $settings); assert_same('mxtest', $ctx->username()); assert_same('h4.domena.pl', $ctx->sourceHost()); assert_same('h4', $ctx->serverName()); }); test('directadmin context prefers trusted configured source server name over host header', function (): void { $settings = Settings::fromArray(['MAIL_LOGIN_SOURCE_SERVER_NAME' => 'h4']); $ctx = DirectAdminContext::fromServer([ 'USERNAME' => 'mxtest', 'HTTP_HOST' => 'evil.example.net:2222', 'SERVER_NAME' => 'h4.domena.pl', 'REMOTE_ADDR' => '198.51.100.10', 'REQUEST_METHOD' => 'GET', ], $settings); assert_same('h4.domena.pl', $ctx->sourceHost()); assert_same('h4', $ctx->serverName()); }); test('directadmin context fails closed without directadmin username', function (): void { try { DirectAdminContext::fromServer([ 'USER' => 'root', 'HTTP_HOST' => 'h4.domena.pl:2222', 'REMOTE_ADDR' => '198.51.100.10', 'REQUEST_METHOD' => 'GET', ], Settings::fromArray([])); throw new RuntimeException('Expected missing USERNAME to fail'); } catch (InvalidArgumentException $e) { assert_contains('username', strtolower($e->getMessage())); } }); test('directadmin context rejects invalid usernames', function (): void { try { DirectAdminContext::fromServer([ 'USERNAME' => '../../root', 'HTTP_HOST' => 'h4.domena.pl:2222', 'REMOTE_ADDR' => '198.51.100.10', 'REQUEST_METHOD' => 'GET', ], Settings::fromArray([])); throw new RuntimeException('Expected invalid username to fail'); } catch (InvalidArgumentException $e) { assert_contains('username', strtolower($e->getMessage())); } }); test('client ip resolves direct remote address by default', function (): void { $settings = Settings::fromArray(['MAIL_LOGIN_CLIENT_IP_MODE' => 'direct']); $ip = ClientIp::resolve([ 'REMOTE_ADDR' => '198.51.100.10', 'HTTP_X_FORWARDED_FOR' => '203.0.113.20', ], $settings); assert_same('198.51.100.10', $ip); }); test('client ip honors forwarded header only from trusted proxy', function (): void { $settings = Settings::fromArray([ 'MAIL_LOGIN_CLIENT_IP_MODE' => 'trusted_proxy', 'MAIL_LOGIN_TRUSTED_PROXIES' => '10.0.0.1', ]); $ip = ClientIp::resolve([ 'REMOTE_ADDR' => '10.0.0.1', 'HTTP_X_FORWARDED_FOR' => '203.0.113.20, 10.0.0.1', ], $settings); assert_same('203.0.113.20', $ip); }); test('client ip supports trusted proxy cidr ranges', function (): void { $settings = Settings::fromArray([ 'MAIL_LOGIN_CLIENT_IP_MODE' => 'trusted_proxy', 'MAIL_LOGIN_TRUSTED_PROXIES' => '10.0.0.0/24', ]); $ip = ClientIp::resolve([ 'REMOTE_ADDR' => '10.0.0.25', 'HTTP_X_FORWARDED_FOR' => '203.0.113.20', ], $settings); assert_same('203.0.113.20', $ip); }); test('source authorizer rejects hosts outside allowlist', function (): void { $settings = Settings::fromArray(['MAIL_LOGIN_ALLOWED_SOURCE_HOSTS' => 'h4.domena.pl']); SourceAuthorizer::assertAllowed('h4.domena.pl', $settings); try { SourceAuthorizer::assertAllowed('evil.example.net', $settings); throw new RuntimeException('Expected disallowed source host to fail'); } catch (RuntimeException $e) { assert_contains('source host', strtolower($e->getMessage())); } }); test('key name uses requested format and truncates long values deterministically', function (): void { assert_same('autologin-h4-mxtest-1782810000', KeyName::build('h4', 'mxtest', 1782810000)); $long = KeyName::build(str_repeat('server-', 15), str_repeat('user-', 15), 1782810000); assert_true(strlen($long) <= 96, 'Key name must be at most 96 chars'); assert_true(str_starts_with($long, 'autologin-')); assert_true((bool)preg_match('/-[a-f0-9]{10}-1782810000$/', $long), 'Long key should contain hash suffix'); });