*/ private array $values; /** * @param array $values */ private function __construct(array $values) { $this->values = $values; $this->validate(); } public static function load(string $path): self { $values = self::defaults(); if (is_file($path)) { $lines = file($path, FILE_IGNORE_NEW_LINES); if ($lines !== false) { foreach ($lines as $line) { $line = trim($line); if ($line === '' || str_starts_with($line, '#') || !str_contains($line, '=')) { continue; } [$key, $value] = explode('=', $line, 2); $values[trim($key)] = trim($value); } } } return new self($values); } /** * @param array $values */ public static function fromArray(array $values): self { return new self(array_merge(self::defaults(), $values)); } /** * @return array */ public static function defaults(): array { return [ 'DEFAULT_ENABLE' => 'true', 'MAIL_LOGIN_METHOD' => 'login_url', 'MAIL_LOGIN_TARGET_NAME' => 'mx1', 'MAIL_LOGIN_TARGET_BASE_URL' => 'https://mx1.mojserwer.pl:2222', 'MAIL_LOGIN_REDIRECT_URL' => '/', 'LOGIN_KEY_TTL' => '30', 'USER_IP_BOUND' => '1', 'MAIL_LOGIN_ALLOWED_SOURCE_HOSTS' => 's1.abc.pl,serwer.mojserwer.pl,serwer2.xxb.ovh', 'MAIL_LOGIN_SOURCE_SERVER_NAME' => '', 'MAIL_LOGIN_CLIENT_IP_MODE' => 'direct', 'MAIL_LOGIN_TRUSTED_PROXIES' => '', 'MAIL_LOGIN_REMOTE_TRANSPORT' => 'ssh', 'MAIL_LOGIN_SSH_HOST' => 'mx1.mojserwer.pl', 'MAIL_LOGIN_SSH_PORT' => '22', 'MAIL_LOGIN_SSH_USER' => 'root', 'MAIL_LOGIN_SSH_IDENTITY' => self::SECRET_DIR . '/mx1_ed25519', 'MAIL_LOGIN_SSH_KNOWN_HOSTS' => self::SECRET_DIR . '/known_hosts', 'MAIL_LOGIN_CONNECT_TIMEOUT_SECONDS' => '5', 'MAIL_LOGIN_RATE_LIMIT_PER_USER_PER_MINUTE' => '5', 'MAIL_LOGIN_RATE_LIMIT_PER_IP_PER_MINUTE' => '20', 'MAIL_LOGIN_ALLOW_PASSWORD_FALLBACK' => 'false', ]; } public static function normalizeHost(string $host): string { $host = trim($host); $host = preg_replace('/^https?:\/\//i', '', $host) ?? $host; $host = preg_replace('/\/.*$/', '', $host) ?? $host; if (str_starts_with($host, '[')) { $end = strpos($host, ']'); if ($end !== false) { return strtolower(substr($host, 1, $end - 1)); } } $host = preg_replace('/:[0-9]+$/', '', $host) ?? $host; return strtolower(trim($host, ". \t\n\r\0\x0B")); } public function getString(string $key, string $default = ''): string { return $this->values[$key] ?? $default; } public function targetBaseUrl(): string { return rtrim($this->getString('MAIL_LOGIN_TARGET_BASE_URL'), '/'); } public function defaultEnable(): bool { return in_array(strtolower($this->getString('DEFAULT_ENABLE', 'true')), ['1', 'true', 'yes', 'on'], true); } public function method(): string { return $this->getString('MAIL_LOGIN_METHOD', 'login_url'); } public function redirectUrl(): string { return $this->getString('MAIL_LOGIN_REDIRECT_URL', '/'); } public function loginKeyTtl(): int { return (int)$this->getString('LOGIN_KEY_TTL', '30'); } public function userIpBound(): bool { return $this->getString('USER_IP_BOUND', '1') === '1'; } /** * @return list */ public function allowedSourceHosts(): array { return $this->csvHosts('MAIL_LOGIN_ALLOWED_SOURCE_HOSTS'); } public function sourceServerName(): string { return $this->sanitizeServerName($this->getString('MAIL_LOGIN_SOURCE_SERVER_NAME', '')); } public function clientIpMode(): string { return $this->getString('MAIL_LOGIN_CLIENT_IP_MODE', 'direct'); } /** * @return list */ public function trustedProxies(): array { return $this->csvNetworks('MAIL_LOGIN_TRUSTED_PROXIES'); } public function sshHost(): string { return self::normalizeHost($this->getString('MAIL_LOGIN_SSH_HOST')); } public function sshPort(): int { return (int)$this->getString('MAIL_LOGIN_SSH_PORT', '22'); } public function sshUser(): string { return $this->getString('MAIL_LOGIN_SSH_USER', 'root'); } public function sshIdentity(): string { return $this->getString('MAIL_LOGIN_SSH_IDENTITY'); } public function sshKnownHosts(): string { return $this->getString('MAIL_LOGIN_SSH_KNOWN_HOSTS'); } public function connectTimeout(): int { return (int)$this->getString('MAIL_LOGIN_CONNECT_TIMEOUT_SECONDS', '5'); } public function rateLimitPerUserPerMinute(): int { return (int)$this->getString('MAIL_LOGIN_RATE_LIMIT_PER_USER_PER_MINUTE', '5'); } public function rateLimitPerIpPerMinute(): int { return (int)$this->getString('MAIL_LOGIN_RATE_LIMIT_PER_IP_PER_MINUTE', '20'); } public function sanitizeServerName(string $name): string { $name = strtolower(trim($name)); $name = preg_replace('/[^a-z0-9-]+/', '-', $name) ?? ''; $name = trim($name, '-'); if ($name === '') { return ''; } return substr($name, 0, 32); } /** * @return list */ private function csvHosts(string $key): array { $raw = $this->getString($key, ''); if ($raw === '') { return []; } $out = []; foreach (explode(',', $raw) as $item) { $host = self::normalizeHost($item); if ($host !== '') { $out[] = $host; } } return array_values(array_unique($out)); } /** * @return list */ private function csvNetworks(string $key): array { $raw = $this->getString($key, ''); if ($raw === '') { return []; } $out = []; foreach (explode(',', $raw) as $item) { $item = trim($item); if ($item === '') { continue; } if (str_contains($item, '/')) { [$ip, $prefix] = explode('/', $item, 2); if (filter_var($ip, FILTER_VALIDATE_IP) && preg_match('/^[0-9]+$/', $prefix)) { $out[] = $ip . '/' . $prefix; } } else { $host = self::normalizeHost($item); if ($host !== '') { $out[] = $host; } } } return array_values(array_unique($out)); } private function validate(): void { $ttlRaw = $this->getString('LOGIN_KEY_TTL', ''); if (!preg_match('/^[0-9]+$/', $ttlRaw)) { throw new InvalidArgumentException('LOGIN_KEY_TTL must be an integer'); } $ttl = (int)$ttlRaw; if ($ttl < 5 || $ttl > 300) { throw new InvalidArgumentException('LOGIN_KEY_TTL must be between 5 and 300 seconds'); } if (!in_array($this->getString('USER_IP_BOUND', ''), ['0', '1'], true)) { throw new InvalidArgumentException('USER_IP_BOUND must be 0 or 1'); } if (!in_array(strtolower($this->getString('DEFAULT_ENABLE', 'true')), ['0', '1', 'true', 'false', 'yes', 'no', 'on', 'off'], true)) { throw new InvalidArgumentException('DEFAULT_ENABLE must be a boolean value'); } if ($this->method() !== 'login_url') { throw new InvalidArgumentException('MAIL_LOGIN_METHOD must be login_url'); } if (!in_array($this->clientIpMode(), ['direct', 'trusted_proxy'], true)) { throw new InvalidArgumentException('MAIL_LOGIN_CLIENT_IP_MODE must be direct or trusted_proxy'); } $base = parse_url($this->targetBaseUrl()); if (!is_array($base) || ($base['scheme'] ?? '') !== 'https' || empty($base['host'])) { throw new InvalidArgumentException('MAIL_LOGIN_TARGET_BASE_URL must be an https URL'); } if ($this->sshPort() < 1 || $this->sshPort() > 65535) { throw new InvalidArgumentException('MAIL_LOGIN_SSH_PORT is invalid'); } if ($this->connectTimeout() < 1 || $this->connectTimeout() > 30) { throw new InvalidArgumentException('MAIL_LOGIN_CONNECT_TIMEOUT_SECONDS is invalid'); } } }