#!/usr/bin/env php
<?php
/* ============================================================
   alpha-maildsn — list the bounce mails (DSNs) sitting in a local mailbox,
   as JSON, read-only. The panel (php-fpm, www-data) calls it via sudo because
   maildirs under /var/mail/vhosts belong to vmail (0750).

     alpha-maildsn scan <mailbox@domain> [since-unix-ts]

   Prints {"mailbox":…,"dsn":[{ts,rcpt,action,status,diag,subject,cid,msgid}]}
   — the delivery-status fields and the ORIGINAL message's headers only, never
   a body. Deliberately includes nothing from the panel tree: a root helper
   must not execute code that www-data can edit.
   ============================================================ */
$root = getenv('ALPHA_MAIL_ROOT') ?: '/var/mail/vhosts';
$cmd  = $argv[1] ?? '';
$box  = strtolower(trim((string) ($argv[2] ?? '')));
$since = (int) ($argv[3] ?? 0);
if ($cmd !== 'scan' || !preg_match('/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/', $box)) { fwrite(STDERR, "usage: alpha-maildsn scan <mailbox@domain> [since-ts]\n"); exit(2); }
[$user, $domain] = explode('@', $box, 2);
$dir = $root . '/' . $domain . '/' . $user;
if (!is_dir($dir)) { echo json_encode(['mailbox' => $box, 'dsn' => [], 'missing' => true]); exit(0); }

function hdr($block, $name) {
  if (!preg_match('/^' . preg_quote($name, '/') . ':[ \t]*(.*(?:\r?\n[ \t]+.*)*)/mi', $block, $m)) return '';
  $v = preg_replace('/\r?\n[ \t]+/', ' ', $m[1]);
  if (strpos($v, '=?') !== false && function_exists('iconv_mime_decode')) { $d = @iconv_mime_decode($v, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8'); if ($d !== false) $v = $d; }
  return trim($v);
}
$out = []; $n = 0;
foreach (['new', 'cur'] as $sub) {
  $d = @opendir("$dir/$sub");
  if (!$d) continue;
  while (($f = readdir($d)) !== false) {
    if ($f[0] === '.') continue;
    $ts = (int) $f;                                                   // maildir names start with the delivery time
    if ($since && $ts && $ts < $since) continue;
    $raw = @file_get_contents("$dir/$sub/$f", false, null, 0, 65536);
    if ($raw === false) continue;
    if (stripos($raw, 'multipart/report') === false && stripos($raw, 'Undelivered Mail Returned') === false
        && stripos($raw, 'Delivery Status Notification') === false && !preg_match('/^Action:\s*failed/mi', $raw)) continue;
    if (!$ts) { $dt = hdr($raw, 'Date'); $ts = $dt !== '' ? (int) @strtotime($dt) : 0; if ($since && $ts && $ts < $since) continue; }
    $rcpt = '';
    foreach (['Final-Recipient', 'Original-Recipient', 'X-Failed-Recipients'] as $h) {
      $v = hdr($raw, $h); if ($v === '') continue;
      if (preg_match('/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i', $v, $m)) { $rcpt = strtolower($m[0]); break; }
    }
    if ($rcpt === '') continue;
    // the original message's headers follow the message/rfc822 (or text/rfc822-headers) part
    // (a regex over the whole file hit PCRE's backtrack limit on real 14 KB DSNs — plain string search instead)
    $inner = '';
    $p = stripos($raw, 'message/rfc822'); if ($p === false) $p = stripos($raw, 'text/rfc822-headers');
    if ($p !== false) { $q = strpos($raw, "\n\n", $p); if ($q === false) $q = strpos($raw, "\r\n\r\n", $p); if ($q !== false) $inner = substr($raw, $q, 12000); }
    $out[] = [
      'ts'      => $ts,
      'rcpt'    => $rcpt,
      'action'  => strtolower(hdr($raw, 'Action')),
      'status'  => preg_replace('/[^\d.]/', '', substr(hdr($raw, 'Status'), 0, 12)),
      'diag'    => mb_substr(hdr($raw, 'Diagnostic-Code'), 0, 200),
      'subject' => mb_substr(hdr($inner, 'Subject'), 0, 160),
      'cid'     => preg_replace('/[^a-z0-9_]/i', '', substr(hdr($inner, 'X-Alpha-Campaign') ?: hdr($inner, 'X-Mailin-Tag'), 0, 40)),
      'msgid'   => substr(hdr($inner, 'Message-ID'), 0, 120),
    ];
    if (++$n >= 20000) break 2;
  }
  closedir($d);
}
echo json_encode(['mailbox' => $box, 'dsn' => $out, 'scanned' => $n], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
