option('dry-run'); // 1) dovecot.conf: dovecot_config_version als erste Zeile $this->fixDovecotConf($dry); // 2) 10-auth.conf: disable_plaintext_auth → auth_allow_cleartext $this->fixAuthConf($dry); // 3) 10-mail.conf: mail_location → mail_driver + mail_path $this->fixMailConf($dry); // 4) 10-master.conf: einzeilige Blöcke aufsplitten $this->fixMasterConf($dry); // 5) 10-ssl.conf: ssl_cert/ssl_key → ssl_server_cert_file/ssl_server_key_file $this->fixSslConf($dry); // 6) auth-sql.conf.ext: neue passdb/userdb-Syntax $this->fixAuthSqlConf($dry); if (!$dry) { $this->info('Fertig. Starte dovecot neu…'); @exec('systemctl restart dovecot 2>&1', $out, $rc); if ($rc === 0) { $this->info('Dovecot erfolgreich gestartet.'); } else { $this->error('Dovecot-Neustart fehlgeschlagen: ' . implode("\n", $out)); $this->line('Prüfen mit: doveconf -n 2>&1'); } } return self::SUCCESS; } private function fixDovecotConf(bool $dry): void { $file = '/etc/dovecot/dovecot.conf'; $content = @file_get_contents($file); if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; } if (str_starts_with(trim($content), 'dovecot_config_version')) { // Falsche Version ersetzen $new = preg_replace('/^dovecot_config_version\s*=\s*\S+/m', 'dovecot_config_version = 2.4.1', $content); } else { $new = "dovecot_config_version = 2.4.1\n" . $content; } if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; } if ($dry) { $this->line(" [dry-run] würde dovecot_config_version ergänzen in {$file}"); return; } file_put_contents($file, $new); $this->info(" aktualisiert: {$file}"); } private function fixMailConf(bool $dry): void { $file = '/etc/dovecot/conf.d/10-mail.conf'; $content = @file_get_contents($file); if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; } $new = preg_replace_callback( '/^mail_location\s*=\s*(\w+):(.+)$/m', function ($m) { return "mail_driver = {$m[1]}\nmail_path = {$m[2]}"; }, $content ); if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; } if ($dry) { $this->line(" [dry-run] würde mail_location aufteilen in {$file}"); return; } file_put_contents($file, $new); $this->info(" aktualisiert: {$file}"); } private function fixMasterConf(bool $dry): void { $file = '/etc/dovecot/conf.d/10-master.conf'; $content = @file_get_contents($file); if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; } // Einzeilige Blöcke "name { key = val }" → mehrzeilig $new = preg_replace_callback( '/^(\s*)(\S+)\s*\{\s*([^}]+)\s*\}(\s*)$/m', function ($m) { $indent = $m[1]; $name = $m[2]; $inner = trim($m[3]); $pairs = preg_split('/\s+(?=\w+=)/', $inner); $body = implode("\n{$indent} ", array_map('trim', $pairs)); return "{$indent}{$name} {\n{$indent} {$body}\n{$indent}}"; }, $content ); if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; } if ($dry) { $this->line(" [dry-run] würde einzeilige Blöcke aufsplitten in {$file}"); return; } file_put_contents($file, $new); $this->info(" aktualisiert: {$file}"); } private function fixSslConf(bool $dry): void { $file = '/etc/dovecot/conf.d/10-ssl.conf'; $content = @file_get_contents($file); if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; } // ssl_cert = → ssl_server_cert_file = /path $new = preg_replace('/^ssl_cert\s*=\s*<(.+)$/m', 'ssl_server_cert_file = $1', $content); $new = preg_replace('/^ssl_key\s*=\s*<(.+)$/m', 'ssl_server_key_file = $1', $new); // Direkte Pfade ohne < (falls schon teilweise migriert) $new = preg_replace('/^ssl_cert\s*=\s*(?!<)(.+)$/m', 'ssl_server_cert_file = $1', $new); $new = preg_replace('/^ssl_key\s*=\s*(?!<)(.+)$/m', 'ssl_server_key_file = $1', $new); // dovecot_storage_version in dovecot.conf (wenn noch nicht vorhanden) $mainConf = '/etc/dovecot/dovecot.conf'; $mainContent = (string) @file_get_contents($mainConf); if (!str_contains($mainContent, 'dovecot_storage_version')) { if (!$dry) { file_put_contents($mainConf, $mainContent . "\ndovecot_storage_version = 2.4.1\n"); $this->info(" dovecot_storage_version ergänzt in {$mainConf}"); } else { $this->line(" [dry-run] würde dovecot_storage_version ergänzen in {$mainConf}"); } } if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; } if ($dry) { $this->line(" [dry-run] würde ssl_cert/ssl_key umbenennen in {$file}"); return; } file_put_contents($file, $new); $this->info(" aktualisiert: {$file}"); } private function fixAuthConf(bool $dry): void { $file = '/etc/dovecot/conf.d/10-auth.conf'; $content = @file_get_contents($file); if ($content === false) { $this->warn("Nicht gefunden: {$file}"); return; } $new = str_replace('disable_plaintext_auth = yes', 'auth_allow_cleartext = no', $content); $new = str_replace('disable_plaintext_auth = no', 'auth_allow_cleartext = yes', $new); if ($new === $content) { $this->line(" ok (unverändert): {$file}"); return; } if ($dry) { $this->line(" [dry-run] würde disable_plaintext_auth ersetzen in {$file}"); return; } file_put_contents($file, $new); $this->info(" aktualisiert: {$file}"); } private function fixAuthSqlConf(bool $dry): void { $file = '/etc/dovecot/conf.d/auth-sql.conf.ext'; $sqlConf = '/etc/dovecot/dovecot-sql.conf.ext'; // Alte Werte aus dovecot-sql.conf.ext lesen $sqlRaw = (string) @file_get_contents($sqlConf); $host = $this->parseSqlValue($sqlRaw, 'host') ?: '127.0.0.1'; $dbname = $this->parseSqlValue($sqlRaw, 'dbname') ?: 'mailwolt'; $user = $this->parseSqlValue($sqlRaw, 'user') ?: 'mailwolt'; $pass = $this->parseSqlValue($sqlRaw, 'password') ?: ''; $scheme = $this->parseSqlValue($sqlRaw, 'default_pass_scheme') ?: 'BLF-CRYPT'; // password_query extrahieren (auch aus Kommentaren) $query = $this->parsePasswordQuery($sqlRaw); $new = "mysql {$host} {\n" . " dbname = {$dbname}\n" . " user = {$user}\n" . " password = {$pass}\n" . "}\n\n" . "passdb sql {\n" . " default_password_scheme = {$scheme}\n" . " query = {$query}\n" . "}\n\n" . "userdb static {\n" . " fields {\n" . " uid = vmail\n" . " gid = vmail\n" . " home = /var/mail/vhosts/%{user|domain}/%{user|username}\n" . " }\n" . "}\n"; if ($dry) { $this->line(" [dry-run] würde {$file} neu schreiben:"); $this->line($new); return; } file_put_contents($file, $new); $this->info(" neu geschrieben: {$file}"); } private function parseSqlValue(string $content, string $key): ?string { // Aus connect-Zeile: connect = host=x dbname=y user=z password=w if (preg_match('/^connect\s*=\s*(.+)/m', $content, $m)) { $connect = $m[1]; if (preg_match('/' . preg_quote($key, '/') . '\s*=\s*(\S+)/i', $connect, $m2)) { return trim($m2[1]); } } // Direkte Zeile: key = value if (preg_match('/^' . preg_quote($key, '/') . '\s*=\s*(.+)/m', $content, $m)) { return trim($m[1]); } return null; } private function parsePasswordQuery(string $content): string { // Auskommentierte oder aktive password_query / query Zeile if (preg_match('/^#?\s*password_query\s*=\s*(.+)/m', $content, $m)) { $q = trim($m[1]); // %u → %{user} $q = str_replace("'%u'", "'%{user}'", $q); return rtrim($q, ';'); } return "SELECT email AS user, password_hash AS password FROM mail_users WHERE email = '%{user}' AND is_active = 1 LIMIT 1"; } }