#!/usr/bin/env php
<?php
/* ============================================================
   alpha-mailstream — give a sending stream its own DKIM identity.

   A "stream" is a subdomain the panel sends FROM: mail.<domain> for the mail a
   person is waiting for (password reset, verification, receipts), news.<domain>
   for the mail they can live without (newsletters, promotions). Mailbox
   providers keep a reputation per authenticated domain, so a bad campaign on
   news. cannot drag mail. down — but only if each subdomain signs with ITS OWN
   DKIM key (d=news.<domain>), not the organisation's. That is what this does:

     enable  <sub.domain>    generate a 2048-bit key (once), add it to the
                             OpenDKIM KeyTable + SigningTable, reload OpenDKIM
     disable <sub.domain>    take it out of the tables (the key file stays)
     status  <sub.domain>…   key / tables / the TXT to publish / is it live

   Prints JSON. Never prints a private key — only the public TXT. Deliberately
   self-contained: a root helper must not execute code www-data can edit.
   The panel (php-fpm, www-data) calls it via sudo.
   ============================================================ */
$KEYS  = '/etc/opendkim/keys';
$KT    = '/etc/opendkim/key.table';
$ST    = '/etc/opendkim/signing.table';
$SEL   = 'mail';
$cmd   = $argv[1] ?? '';
$args  = array_slice($argv, 2);
function out($a) { echo json_encode($a, JSON_UNESCAPED_SLASHES), "\n"; }
function die_usage() { fwrite(STDERR, "usage: alpha-mailstream enable|disable|status <sub.domain> [...]\n"); exit(2); }
if (!in_array($cmd, ['enable', 'disable', 'status'], true) || !$args) die_usage();

$org = strtolower(trim((string) shell_exec('postconf -h mydomain 2>/dev/null')));
if ($org === '' || !preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/', $org)) { out(['ok' => false, 'msg' => 'Postfix has no mydomain.']); exit(1); }

/* only a single-label subdomain of THIS server's domain — never an arbitrary name */
function valid_sub($d, $org) { return (bool) preg_match('/^[a-z0-9]([a-z0-9-]{0,28}[a-z0-9])?\.' . preg_quote($org, '/') . '$/', $d); }
foreach ($args as $d) if (!valid_sub(strtolower($d), $org)) { out(['ok' => false, 'msg' => "refusing: $d is not a subdomain of $org"]); exit(1); }

function txt_value($file) {
  // opendkim-genkey writes a BIND snippet: joined quoted strings are the record
  $s = (string) @file_get_contents($file);
  if (!preg_match_all('/"([^"]*)"/', $s, $m)) return '';
  return trim(implode('', $m[1]));
}
function dns_txt($name) {
  $o = []; @exec('dig +short TXT ' . escapeshellarg($name) . ' 2>/dev/null', $o);
  $v = [];
  foreach ($o as $l) { $l = trim($l); if ($l === '') continue; if (preg_match_all('/"([^"]*)"/', $l, $m)) $v[] = implode('', $m[1]); else $v[] = $l; }
  return $v;
}
function dkim_p($txt) { return preg_match('/p=([A-Za-z0-9+\/=]+)/', str_replace(' ', '', $txt), $m) ? $m[1] : ''; }
function table_has($file, $needle) { return is_file($file) && strpos((string) file_get_contents($file), $needle) !== false; }
function status_of($d, $KEYS, $KT, $ST, $SEL) {
  $dir = "$KEYS/$d";
  $key = is_file("$dir/$SEL.private");
  $val = $key ? txt_value("$dir/$SEL.txt") : '';
  $name = "$SEL._domainkey.$d";
  $live = dns_txt($name);
  $liveTxt = ''; foreach ($live as $t) if (stripos($t, 'v=DKIM1') === 0 || strpos($t, 'p=') !== false) { $liveTxt = $t; break; }
  $want = dkim_p($val); $have = dkim_p($liveTxt);
  return [
    'domain' => $d, 'selector' => $SEL,
    'key' => $key,
    'tables' => table_has($KT, "$name ") && table_has($ST, "*@$d "),
    'dns_name' => $name, 'dns_value' => $val,
    'published' => $liveTxt !== '',
    'match' => $want !== '' && $have !== '' && $want === $have,
  ];
}

if ($cmd === 'status') {
  $o = ['ok' => true, 'org' => $org, 'domains' => []];
  foreach ($args as $d) $o['domains'][strtolower($d)] = status_of(strtolower($d), $KEYS, $KT, $ST, $SEL);
  out($o); exit(0);
}

$d = strtolower($args[0]);
$dir = "$KEYS/$d";
if ($cmd === 'enable') {
  if (!is_file("$dir/$SEL.private")) {
    if (!is_dir($dir) && !@mkdir($dir, 0750, true)) { out(['ok' => false, 'msg' => "cannot create $dir"]); exit(1); }
    $rc = 0; $o = [];
    exec('opendkim-genkey -b 2048 -r -d ' . escapeshellarg($d) . ' -s ' . escapeshellarg($SEL) . ' -D ' . escapeshellarg($dir) . ' 2>&1', $o, $rc);
    if ($rc !== 0 || !is_file("$dir/$SEL.private")) { out(['ok' => false, 'msg' => 'opendkim-genkey failed: ' . implode(' ', $o)]); exit(1); }
  }
  @chown($dir, 'opendkim'); @chgrp($dir, 'opendkim'); @chmod($dir, 0750);
  foreach (["$dir/$SEL.private", "$dir/$SEL.txt"] as $f) { @chown($f, 'opendkim'); @chgrp($f, 'opendkim'); @chmod($f, 0600); }
  /* KeyTable: one line per selector@domain. SigningTable (refile): first match wins,
     so the stream's own line goes ABOVE the organisation's wildcard. */
  $kt = is_file($KT) ? (string) file_get_contents($KT) : '';
  $ktLine = "$SEL._domainkey.$d $d:$SEL:$dir/$SEL.private";
  $ktRows = array_values(array_filter(array_map('rtrim', explode("\n", $kt)), fn($l) => $l !== '' && strpos($l, "$SEL._domainkey.$d ") !== 0));
  $ktRows[] = $ktLine;
  file_put_contents($KT, implode("\n", $ktRows) . "\n");
  $st = is_file($ST) ? (string) file_get_contents($ST) : '';
  $stRows = array_values(array_filter(array_map('rtrim', explode("\n", $st)), fn($l) => $l !== '' && strpos($l, "*@$d ") !== 0));
  array_unshift($stRows, "*@$d $SEL._domainkey.$d");
  file_put_contents($ST, implode("\n", $stRows) . "\n");
  @chmod($KT, 0644); @chmod($ST, 0644);
  exec('systemctl reload opendkim 2>&1 || systemctl restart opendkim 2>&1', $o2, $rc2);
  $s = status_of($d, $KEYS, $KT, $ST, $SEL);
  out(['ok' => true, 'msg' => "DKIM identity ready for $d — publish the TXT record, then re-check.", 'reloaded' => $rc2 === 0] + $s);
  exit(0);
}

if ($cmd === 'disable') {
  foreach ([$KT => "$SEL._domainkey.$d ", $ST => "*@$d "] as $f => $prefix) {
    if (!is_file($f)) continue;
    $rows = array_values(array_filter(array_map('rtrim', explode("\n", (string) file_get_contents($f))), fn($l) => $l !== '' && strpos($l, $prefix) !== 0));
    file_put_contents($f, implode("\n", $rows) . "\n");
  }
  exec('systemctl reload opendkim 2>&1 || systemctl restart opendkim 2>&1', $o2, $rc2);
  out(['ok' => true, 'msg' => "$d no longer signs with its own key (the key file is kept).", 'reloaded' => $rc2 === 0] + status_of($d, $KEYS, $KT, $ST, $SEL));
  exit(0);
}
