safeKey($domain->domain);
// $selKey = $this->safeKey($selector, 32);
$dirKey = $this->dirKeyFor($domain);
$selKey = preg_replace('/[^A-Za-z0-9._-]/', '_', substr($selector, 0, 32)); // schlicht & stabil
// Disk "local" zeigt bei dir auf storage/app/private (siehe Kommentar in deinem Code)
$disk = Storage::disk('local');
$baseRel = "dkim/{$dirKey}";
// unsere Dateien (alle Varianten, damit sowohl App als auch OpenDKIM glücklich sind)
$privPemRel = "{$baseRel}/{$selKey}.pem"; // PKCS#1/PKCS#8 (App-Backup)
$pubPemRel = "{$baseRel}/{$selKey}.pub"; // Public PEM (App-Backup)
$privOKRel = "{$baseRel}/{$selKey}.private"; // <-- OpenDKIM erwartet *genau das*
$dnsTxtRel = "{$baseRel}/{$selKey}.txt"; // TXT-String "v=DKIM1; k=rsa; p=..."
// Absolute Pfade
$privPemAbs = method_exists($disk, 'path') ? $disk->path($privPemRel) : storage_path('app/private/'.$privPemRel);
$pubPemAbs = method_exists($disk, 'path') ? $disk->path($pubPemRel) : storage_path('app/private/'.$pubPemRel);
$privOKAbs = method_exists($disk, 'path') ? $disk->path($privOKRel) : storage_path('app/private/'.$privOKRel);
$dnsTxtAbs = method_exists($disk, 'path') ? $disk->path($dnsTxtRel) : storage_path('app/private/'.$dnsTxtRel);
// Wenn schon vollständig vorhanden → direkt zurück
if ($disk->exists($privOKRel) && $disk->exists($dnsTxtRel)) {
$privatePem = $disk->exists($privPemRel) ? $disk->get($privPemRel) : $disk->get($privOKRel);
$publicPem = $disk->exists($pubPemRel) ? $disk->get($pubPemRel) : null;
$dnsTxt = $disk->get($dnsTxtRel);
return [
'selector' => $selKey,
'priv_path' => $privOKAbs,
'pub_path' => $pubPemAbs,
'private_pem' => $privatePem,
'public_pem' => $publicPem,
'dns_name' => "{$selKey}._domainkey",
'dns_txt' => $dnsTxt,
'bits' => $bits,
];
}
// Neu generieren
$disk->makeDirectory($baseRel);
$res = openssl_pkey_new([
'private_key_type' => OPENSSL_KEYTYPE_RSA,
'private_key_bits' => $bits,
]);
if ($res === false) {
throw new RuntimeException('DKIM: openssl_pkey_new() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
}
$privatePem = '';
if (!openssl_pkey_export($res, $privatePem)) {
throw new RuntimeException('DKIM: openssl_pkey_export() fehlgeschlagen: ' . (openssl_error_string() ?: 'unbekannt'));
}
$details = openssl_pkey_get_details($res);
if ($details === false || empty($details['key'])) {
throw new RuntimeException('DKIM: Public Key konnte nicht gelesen werden.');
}
$publicPem = $details['key'];
$publicBase64 = trim(preg_replace('/-----(BEGIN|END)\s+PUBLIC KEY-----|\s+/', '', $publicPem));
if (strlen($publicBase64) < 300) {
throw new RuntimeException('DKIM: Public Key zu kurz – vermutlich Parsing-Fehler.');
}
$dnsTxt = "v=DKIM1; k=rsa; p={$publicBase64}";
// Dateien schreiben (App + OpenDKIM)
if (!$disk->put($privPemRel, $privatePem)) throw new RuntimeException("DKIM: Private-Key schreiben fehlgeschlagen: {$privPemRel}");
if (!$disk->put($pubPemRel, $publicPem)) throw new RuntimeException("DKIM: Public-Key schreiben fehlgeschlagen: {$pubPemRel}");
if (!$disk->put($privOKRel, $privatePem)) throw new RuntimeException("DKIM: OpenDKIM-Key schreiben fehlgeschlagen: {$privOKRel}");
if (!$disk->put($dnsTxtRel, $dnsTxt)) throw new RuntimeException("DKIM: DNS-TXT schreiben fehlgeschlagen: {$dnsTxtRel}");
// Dateirechte (best effort)
@chmod($privPemAbs, 0600);
@chmod($privOKAbs, 0600);
@chmod($pubPemAbs, 0640);
@chmod($dnsTxtAbs, 0644);
// DB pflegen
DkimKey::updateOrCreate(
['domain_id' => $domain->id, 'selector' => $selKey],
[
'private_key_pem' => $privatePem,
'public_key_txt' => $publicBase64,
'is_active' => true,
]
);
// OpenDKIM einhängen (wenn Helper existiert)
$helper = '/usr/local/sbin/mailwolt-install-dkim';
// if (is_executable($helper)) {
// $cmd = [
// 'sudo','-n', $helper,
// $domain->domain,
// $selKey,
// $privOKAbs, // …/storage/app/private/dkim/
/.private
// $dnsTxtAbs // …/storage/app/private/dkim//.txt
// ];
//
// $res = Process::timeout(30)->run($cmd);
//
// if ($res->failed()) {
// Log::error('DKIM install failed', [
// 'cmd' => implode(' ', $cmd),
// 'exit' => $res->exitCode(),
// 'out' => $res->output(),
// 'err' => $res->errorOutput(),
// ]);
// throw new RuntimeException(
// 'OpenDKIM-Install fehlgeschlagen: '.$res->errorOutput()
// );
// }
//
// // OpenDKIM neu laden (falls der Helper das nicht selbst tut)
// Process::run(['sudo','-n','systemctl','reload','opendkim']);
// }
$helper = '/usr/local/sbin/mailwolt-install-dkim';
Log::debug('DKIM helper call', [
'as' => trim(Process::run(['whoami'])->output()),
'helper' => $helper,
'exists' => is_file($helper),
// KEINE privaten Keys loggen!
]);
if (is_file($helper)) {
$cmd = [
'sudo','-n', $helper,
$domain->domain,
$selKey,
$privOKAbs,
$dnsTxtAbs,
];
$res = Process::timeout(30)->run($cmd);
Log::info('DKIM install exit', [
'cmd' => implode(' ', $cmd),
'exit' => $res->exitCode(),
'out' => $res->output(),
'err' => $res->errorOutput(),
]);
if ($res->failed()) {
throw new RuntimeException('OpenDKIM-Install fehlgeschlagen: '.$res->errorOutput());
}
Process::run(['sudo','-n','/usr/bin/systemctl','reload','opendkim']);
} else {
Log::warning('DKIM helper not found', ['path' => $helper]);
}
return [
'selector' => $selKey,
'priv_path' => $privOKAbs,
'pub_path' => $pubPemAbs,
'private_pem' => $privatePem,
'public_pem' => $publicPem,
'dns_name' => "{$selKey}._domainkey",
'dns_txt' => $dnsTxt,
'bits' => $bits,
];
}
protected function dirKeyFor(Domain $domain, int $max = 64): string
{
$raw = $domain->domain; // <<< WICHTIG: Domain-String, NICHT die ID!
$san = preg_replace('/[^A-Za-z0-9._-]/', '_', $raw);
if ($san === '') $san = 'unknown';
if (strlen($san) > $max) {
$san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12);
}
return $san;
}
// selector optional: wenn null → alle Selector der Domain löschen
public function removeForDomain(Domain|string $domain, ?string $selector = null): void
{
$name = $domain instanceof Domain ? $domain->domain : $domain;
// Selektoren ermitteln
if (is_null($selector)) {
$keys = $domain instanceof Domain
? $domain->dkimKeys()->pluck('selector')->all()
: \App\Models\DkimKey::whereHas('domain', fn($q) => $q->where('domain', $name))
->pluck('selector')->all();
$keys = $keys ?: ['mwl1'];
} else {
$keys = [$selector];
}
foreach ($keys as $sel) {
$cmd = ['sudo','-n','/usr/local/sbin/mailwolt-remove-dkim',$name,$sel];
$res = \Illuminate\Support\Facades\Process::timeout(30)->run($cmd);
Log::info('DKIM remove exit', [
'domain' => $name,
'selector' => $sel,
'exit' => $res->exitCode(),
'out' => $res->output(),
'err' => $res->errorOutput(),
]);
if ($res->failed()) {
throw new \RuntimeException('OpenDKIM-Remove fehlgeschlagen: '.$res->errorOutput());
}
}
// Dienst neu laden (best effort)
\Illuminate\Support\Facades\Process::run(['sudo','-n','/bin/systemctl','reload','opendkim']);
}
// public function removeForDomain(Domain|string $domain, ?string $selector = null): void
// {
// $name = $domain instanceof Domain ? $domain->domain : $domain;
//
// if (is_null($selector)) {
// // alle Selector aus DB holen und nacheinander entfernen
// $keys = $domain instanceof Domain
// ? $domain->dkimKeys()->pluck('selector')->all()
// : \App\Models\DkimKey::whereHas('domain', fn($q) => $q->where('domain', $name))
// ->pluck('selector')->all();
//
// $keys = $keys ?: ['mwl1']; // notfalls versuchen wir Standard
// } else {
// $keys = [$selector];
// }
//
// foreach ($keys as $sel) {
// Process::run(['sudo','-n','/usr/local/sbin/mailwolt-remove-dkim',$name,$sel]);
// }
//
// // Dienst neu laden (ohne Fehler abbrechen)
// Process::run(['sudo','-n','/bin/systemctl','reload','opendkim']);
//
// $res = Process::timeout(30)->run($cmd);
// Log::info('DKIM install exit', [
// 'exit' => $res->exitCode(),
// 'out' => $res->output(),
// 'err' => $res->errorOutput(),
// ]);
// }
// public function removeForDomain(Domain|string $domain, ?string $selector = null): void
// {
// $name = $domain instanceof \App\Models\Domain ? $domain->domain : $domain;
// $selector = $selector ?: (string) config('mailpool.defaults.dkim_selector', 'mwl1');
//
// // Root-Helper ausführen
// $p = Process::run([
// 'sudo','-n','/usr/local/sbin/mailwolt-remove-dkim',
// $name, $selector
// ]);
// if (!$p->successful()) {
// throw new \RuntimeException('mailwolt-remove-dkim failed: '.$p->errorOutput());
// }
//
// // OpenDKIM neu laden
// Process::run(['sudo','-n','/usr/bin/systemctl','reload','opendkim']);
// }
// public function removeForDomain(Domain|string $domain): void
// {
// $domainName = $domain instanceof Domain ? $domain->domain : $domain;
// $keyTable = '/etc/opendkim/KeyTable';
// $signTable = '/etc/opendkim/SigningTable';
// $keyDir = "/etc/opendkim/keys/{$domainName}";
//
// // Tabellen bereinigen
// foreach ([$keyTable, $signTable] as $file) {
// if (is_file($file)) {
// $lines = file($file, FILE_IGNORE_NEW_LINES);
// $filtered = array_filter($lines, fn($l) => !str_contains($l, $domainName));
// file_put_contents($file, implode(PHP_EOL, $filtered) . PHP_EOL);
// }
// }
//
// // Key-Verzeichnis löschen
// if (is_dir($keyDir)) {
// \Illuminate\Support\Facades\File::deleteDirectory($keyDir);
// }
// }
// protected function safeKey($value, int $max = 64): string
// {
// if (is_object($value)) {
// if (isset($value->id)) $value = $value->id;
// elseif (method_exists($value, 'getKey')) $value = $value->getKey();
// else $value = json_encode($value);
// }
// $raw = (string) $value;
// $san = preg_replace('/[^A-Za-z0-9._-]/', '_', $raw);
// if ($san === '' ) $san = 'unknown';
// if (strlen($san) > $max) {
// $san = substr($san, 0, $max - 13) . '_' . substr(sha1($raw), 0, 12);
// }
// return $san;
// }
//
// protected static function extractPublicKeyBase64(string $pem): string
// {
// // Hole den Body zwischen den Headern (multiline, dotall)
// if (!preg_match('/^-+BEGIN PUBLIC KEY-+\r?\n(.+?)\r?\n-+END PUBLIC KEY-+\s*$/ms', $pem, $m)) {
// throw new \RuntimeException('DKIM: Ungültiges Public-Key-PEM (Header/Footers nicht gefunden).');
// }
//
// // Whitespace entfernen → reines Base64
// $b64 = preg_replace('/\s+/', '', $m[1]);
//
// if ($b64 === '' || base64_decode($b64, true) === false) {
// throw new \RuntimeException('DKIM: Public Key Base64 ist leer/ungültig.');
// }
//
// return $b64;
// }
}