<?php
/**
 * AcellePMTA Tracking Relay — deployed to /var/www/scripts/tracking.php
 *
 * Handles all Acelle tracking URLs served from the MTA server's domain.
 * Acelle's URL/IP is never exposed to subscribers at any point.
 *
 * URL patterns:
 *   /p/{b64_msgid}/open  — open pixel → 1px GIF + background tracking to Acelle
 *   /p/click/            — click redirect → real URL extracted from url= param
 *                          subscriber redirected immediately (no Acelle needed)
 *                          then background tracking fired to Acelle
 *   /u/{token}           — unsubscribe form → POST to Acelle plugin endpoint
 *
 * $p_api is replaced at deploy time with the AES-encrypted Acelle base URL.
 * $p_tracking_enc_key is replaced with the raw encryption key.
 */

define('DS', DIRECTORY_SEPARATOR);
ini_set('default_socket_timeout', '5');

// ── Helpers ───────────────────────────────────────────────────────────────────

function apmta_decrypt(string $value): string
{
    $encrypted = base64_decode($value);
    $salt      = substr($encrypted, 0, 32);
    $data      = substr($encrypted, 32);
    $salted    = $dx = '';
    while (strlen($salted) < 48) {
        $dx      = md5($dx . '2dabf62cbf8cc07d' . $salt, true);
        $salted .= $dx;
    }
    return (string) openssl_decrypt(
        $data, 'aes-256-cbc',
        substr($salted, 0, 32), OPENSSL_RAW_DATA, substr($salted, 32, 16)
    );
}

function apmta_b64url_decode(string $s): string
{
    $pad = (4 - strlen($s) % 4) % 4;
    return (string) base64_decode(strtr($s, '-_', '+/') . str_repeat('=', $pad));
}

// ── Link Types (acelle/linktypes) decode — KEEP IN SYNC WITH LinkCodec.php ──────
// Patterned click/open links point at this box as /lt/...; we decode them back to the
// canonical (base64url message_id, base64url destination) and hand off to the SAME
// handlers as native /p/ links. $p_lt_enc_key is replaced at deploy with the raw
// linktypes.enc_key (empty string when no encrypted patterns are used).

function apmta_lt_evp_decrypt(string $token, string $key): string
{
    $blob = apmta_b64url_decode($token);
    // No key, or not an OpenSSL "Salted__" envelope → it was a plain base64url token.
    if ($key === '' || strncmp($blob, 'Salted__', 8) !== 0) {
        return apmta_b64url_decode($token);
    }
    $salt   = substr($blob, 8, 8);
    $cipher = substr($blob, 16);
    $data = $prev = '';
    while (strlen($data) < 48) {            // EVP_BytesToKey (MD5): 32-byte key + 16-byte IV
        $prev = md5($prev . $key . $salt, true);
        $data .= $prev;
    }
    return (string) openssl_decrypt($cipher, 'aes-256-cbc', substr($data, 0, 32), OPENSSL_RAW_DATA, substr($data, 16 + 16, 16));
}

/** RFC 4648 Base32 decode (padding-tolerant) — mirrors LinkCodec::b32decode. */
function apmta_lt_b32decode(string $s): string
{
    static $map = null;
    if ($map === null) { $map = array_flip(str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')); }
    $s = strtoupper((string) preg_replace('/[^A-Za-z2-7]/', '', $s));
    $out = ''; $val = 0; $bits = 0;
    for ($i = 0, $n = strlen($s); $i < $n; $i++) {
        if (!isset($map[$s[$i]])) { continue; }
        $val = ($val << 5) | $map[$s[$i]]; $bits += 5;
        if ($bits >= 8) { $out .= chr(($val >> ($bits - 8)) & 255); $bits -= 8; }
    }
    return $out;
}

/** Tagged opaque blob ('b'|'s'|'e'|'h'|'3' + token) → raw payload string. */
function apmta_lt_decode_blob(string $blob, string $key): string
{
    $tag = substr($blob, 0, 1);
    $tok = substr($blob, 1);
    switch ($tag) {
        case 'b': return apmta_b64url_decode($tok);
        // slug: strip cosmetic chunk dashes, restore '.'→'-', then base64url-decode
        case 's': return apmta_b64url_decode(str_replace('.', '-', str_replace('-', '', $tok)));
        case 'e': return apmta_lt_evp_decrypt($tok, $key);
        case 'h': return (string) (hex2bin($tok) ?: '');
        case '3': return apmta_lt_b32decode($tok);
        default:  return apmta_b64url_decode($blob);   // tolerate untagged
    }
}

/** Opaque click blob → ['m'=>msgIdB64, 'u'=>urlB64]. */
function apmta_lt_decode_opaque(string $blob, string $key): array
{
    $payload = apmta_lt_decode_blob($blob, $key);
    $parts   = explode('~', $payload, 2);   // SEP '~' — not in the base64url alphabet
    return ['m' => $parts[0] ?? '', 'u' => $parts[1] ?? ''];
}

/** Attribute-style click query (?url/message_id | ?u/m | ?q | ?e | ?id) → ['m'=>..,'u'=>..]. */
function apmta_lt_decode_attr(array $q, string $key): array
{
    if (isset($q['e'])) {
        $parts = explode('~', apmta_lt_evp_decrypt($q['e'], $key), 2);
        return ['m' => $parts[0] ?? '', 'u' => $parts[1] ?? ''];
    }
    if (isset($q['q'])) {
        $parts = explode('~', $q['q'], 2);
        return ['m' => $parts[0] ?? '', 'u' => $parts[1] ?? ''];
    }
    if (isset($q['id'])) { // decoy: packed blob hidden among marketing params
        $parts = explode('~', apmta_b64url_decode($q['id']), 2);
        return ['m' => $parts[0] ?? '', 'u' => $parts[1] ?? ''];
    }
    return [
        'm' => $q['message_id'] ?? $q['m'] ?? '',
        'u' => $q['url'] ?? $q['u'] ?? '',
    ];
}

/** Attribute-style OPEN query (?message_id | ?m | ?q | ?e | ?id) → msgIdB64. */
function apmta_lt_decode_open_attr(array $q, string $key): string
{
    if (isset($q['e'])) { return apmta_lt_evp_decrypt($q['e'], $key); }
    return $q['message_id'] ?? $q['m'] ?? $q['q'] ?? $q['id'] ?? '';
}

/** Segmented blob: reassemble path segments → raw base64url → decoded payload. */
function apmta_lt_decode_segment(string $captured): string
{
    return apmta_b64url_decode(str_replace('/', '', $captured));
}

/** Unwrap a JWT-look token (body.signature) → inner payload. Mirrors LinkCodec::jwtInner(). */
function apmta_lt_jwt_inner(string $j): string
{
    $body = explode('.', $j, 2)[0] ?? '';
    $obj  = json_decode(apmta_b64url_decode($body), true);
    return is_array($obj) ? (string) ($obj['u'] ?? '') : '';
}

/** Generic token decoder by encoding name — mirrors PatternEngine::decodeTok(). */
function apmta_lt_decode_enc(string $t, string $enc, string $key): string
{
    switch ($enc) {
        case 'b64url': return apmta_b64url_decode($t);
        case 'slug':   return apmta_b64url_decode(str_replace('.', '-', str_replace('-', '', $t)));
        case 'aes':    return apmta_lt_evp_decrypt($t, $key);
        case 'hex':    return (string) (hex2bin($t) ?: '');
        case 'b32':    return apmta_lt_b32decode($t);
        default:       return $t;
    }
}

/**
 * Generic spec-driven decode of a box URL using a decode table (prefix → form). Mirrors
 * PatternEngine::decode(). Returns ['dir'=>click|open,'m'=>..,'u'=>..] or null.
 */
function apmta_lt_dispatch(array $table, string $uri, string $query, string $key): ?array
{
    $seg = explode('/', $uri);
    $p0  = $seg[0] ?? '';
    if (!isset($table[$p0])) {
        return null;
    }
    $f = $table[$p0];
    $m = $u = '';
    switch ($f['kind'] ?? '') {
        case 'splitpath': $m = $seg[1] ?? ''; $u = $seg[2] ?? ''; break;
        case 'single':    $m = $seg[1] ?? ''; break;
        case 'token':
            $tok = (int) ($f['chunk'] ?? 0) > 0 ? implode('', array_slice($seg, 1)) : ($seg[1] ?? '');
            if (!empty($f['sep'])) { $tok = str_replace($f['sep'], '', $tok); }   // drop group separators
            $raw = apmta_lt_decode_enc($tok, $f['enc'] ?? 'none', $key);
            if (($f['dir'] ?? '') === 'click') { $x = explode('~', $raw, 2); $m = $x[0] ?? ''; $u = $x[1] ?? ''; }
            else { $m = $raw; }
            break;
        case 'query':
            parse_str($query, $qp);
            foreach (($f['params'] ?? []) as $pair) {
                $name = $pair[0] ?? ''; $src = $pair[1] ?? ''; $v = $qp[$name] ?? '';
                switch ($src) {
                    case 'U': $u = $v; break;
                    case 'M': $m = $v; break;
                    case 'MU':    $x = explode('~', $v, 2); $m = $x[0] ?? ''; $u = $x[1] ?? ''; break;
                    case 'B64MU': $x = explode('~', apmta_b64url_decode($v), 2); $m = $x[0] ?? ''; $u = $x[1] ?? ''; break;
                    case 'AESMU': $x = explode('~', apmta_lt_evp_decrypt($v, $key), 2); $m = $x[0] ?? ''; $u = $x[1] ?? ''; break;
                    case 'AESM':  $m = apmta_lt_evp_decrypt($v, $key); break;
                    case 'JWTAESMU': $x = explode('~', apmta_lt_evp_decrypt(apmta_lt_jwt_inner($v), $key), 2); $m = $x[0] ?? ''; $u = $x[1] ?? ''; break;
                    case 'JWTAESM':  $m = apmta_lt_evp_decrypt(apmta_lt_jwt_inner($v), $key); break;
                    case 'JWTMU':    $x = explode('~', apmta_b64url_decode(apmta_lt_jwt_inner($v)), 2); $m = $x[0] ?? ''; $u = $x[1] ?? ''; break;
                }
            }
            break;
        default: return null;
    }
    return ['dir' => $f['dir'] ?? 'click', 'm' => $m, 'u' => $u];
}

/** Box-style: decode one family token (b|a|e|h|k) → raw payload. */
function apmta_lt_unblob(string $fam, string $token, string $key): string
{
    switch ($fam) {
        case 'b': return apmta_b64url_decode($token);
        case 'a': return apmta_b64url_decode(str_replace('.', '-', str_replace('-', '', $token)));
        case 'e': return apmta_lt_evp_decrypt($token, $key);
        case 'h': return (string) (hex2bin($token) ?: '');
        case 'k': return apmta_lt_b32decode($token);
        default:  return apmta_b64url_decode($token);
    }
}

/** Box-style attribute CLICK query (?url/message_id | ?u/m | ?d | ?e | ?id) → ['m','u']. */
function apmta_lt_attr2(array $q, string $key): array
{
    if (isset($q['e']))  { $x = explode('~', apmta_lt_evp_decrypt($q['e'], $key), 2); return ['m'=>$x[0]??'','u'=>$x[1]??'']; }
    if (isset($q['d']))  { $x = explode('~', $q['d'], 2);                              return ['m'=>$x[0]??'','u'=>$x[1]??'']; }
    if (isset($q['id'])) { $x = explode('~', apmta_b64url_decode($q['id']), 2);        return ['m'=>$x[0]??'','u'=>$x[1]??'']; }
    return ['m'=>$q['message_id']??$q['m']??'', 'u'=>$q['url']??$q['u']??''];
}

/** Box-style attribute OPEN query → msgIdB64. */
function apmta_lt_open_attr2(array $q, string $key): string
{
    if (isset($q['e'])) { return apmta_lt_evp_decrypt($q['e'], $key); }
    return $q['message_id'] ?? $q['m'] ?? $q['d'] ?? $q['id'] ?? '';
}

function apmta_subscriber_ip(): string
{
    foreach (['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $k) {
        $ip = trim(explode(',', $_SERVER[$k] ?? '')[0]);
        if ($ip && filter_var($ip, FILTER_VALIDATE_IP)) {
            return $ip;
        }
    }
    return '0.0.0.0';
}

/**
 * Fire a GET to Acelle in the background after the response is already sent.
 * Forwards subscriber IP so Acelle logs correct geo-analytics.
 */
function apmta_fire_get(string $url, string $ip, string $agent): void
{
    $disabled = array_map('trim', explode(',', (string) ini_get('disable_functions')));
    if (function_exists('exec') && !in_array('exec', $disabled)) {
        @exec(
            'nohup curl -s -o /dev/null -m 5 ' .
            '--user-agent ' . escapeshellarg($agent ?: 'AcellePMTA/1.0') . ' ' .
            '-H ' . escapeshellarg("X-Forwarded-For: {$ip}") . ' ' .
            '-H ' . escapeshellarg("X-Real-IP: {$ip}") . ' ' .
            escapeshellarg($url) . ' > /dev/null 2>&1 &'
        );
        return;
    }

    // Fallback: non-blocking stream socket
    if (function_exists('fastcgi_finish_request')) {
        fastcgi_finish_request();
    }
    $p = parse_url($url);
    if (!$p || empty($p['host'])) return;
    $prefix = ($p['scheme'] ?? '') === 'https' ? 'ssl://' : '';
    $port   = $p['port'] ?? ($p['scheme'] === 'https' ? 443 : 80);
    $path   = ($p['path'] ?? '/') . (isset($p['query']) ? '?' . $p['query'] : '');
    $fp     = @stream_socket_client("{$prefix}{$p['host']}:{$port}", $e1, $e2, 3);
    if ($fp) {
        @fwrite($fp,
            "GET {$path} HTTP/1.1\r\n" .
            "Host: {$p['host']}\r\n" .
            "User-Agent: " . ($agent ?: 'AcellePMTA/1.0') . "\r\n" .
            "X-Forwarded-For: {$ip}\r\n" .
            "X-Real-IP: {$ip}\r\n" .
            "Connection: close\r\n\r\n"
        );
        @fclose($fp);
    }
}

/**
 * Synchronous POST — used for unsubscribe where we need a success/failure
 * response before rendering the confirmation page to the subscriber.
 */
function apmta_post(string $url, array $data): array
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => http_build_query($data),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => true,   // verify TLS to prevent MITM on unsub relay
        CURLOPT_SSL_VERIFYHOST => 2,
        CURLOPT_TIMEOUT        => 5,
    ]);
    $resp   = curl_exec($ch);
    $errMsg = curl_error($ch);
    curl_close($ch);
    if ($resp === false || $errMsg !== '') {
        return ['ok' => false, 'error' => 'Connection error'];
    }
    return (array) (json_decode((string) $resp, true) ?? []);
}

function apmta_1px_gif(): string
{
    return base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
}

/**
 * Serve the open pixel and report the open to Acelle in the background.
 * $msgId is already base64url-encoded — passed straight through to Acelle.
 * Shared by the native /p/{msgid}/open route and the patterned /lt/o* routes.
 */
function apmta_handle_open(string $aceBase, string $msgId, string $ip, string $agent): void
{
    header('Content-Type: image/gif');
    header('Content-Length: 35');
    header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
    header('Pragma: no-cache');
    header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
    echo apmta_1px_gif();

    apmta_fire_get("{$aceBase}/p/{$msgId}/open", $ip, $agent);
}

/**
 * Redirect the subscriber to the real destination and record the click in Acelle.
 * $b64Url / $b64Msg are the canonical base64url values (same as native /p/click).
 * Shared by the native /p/click route and the patterned /lt/c* routes.
 */
function apmta_handle_click(string $aceBase, string $b64Url, string $b64Msg, string $ip, string $agent): void
{
    $realUrl = $b64Url ? apmta_b64url_decode($b64Url) : '';
    $scheme  = parse_url($realUrl, PHP_URL_SCHEME) ?? '';

    // Validate: real URL must be http/https — blocks open-redirect abuse
    if ($realUrl && filter_var($realUrl, FILTER_VALIDATE_URL) && in_array($scheme, ['http', 'https'])) {
        header('Location: ' . $realUrl, true, 302);
        header('Cache-Control: no-store');
        header('Content-Length: 0');

        // Record click in Acelle in the background — using Acelle's native param names
        // so the relay is indistinguishable from a standard click.
        apmta_fire_get(
            $aceBase . '/p/click/?' . http_build_query(['url' => $b64Url, 'message_id' => $b64Msg]),
            $ip,
            $agent
        );
    } else {
        header('HTTP/1.1 400 Bad Request');
        echo 'Invalid or missing tracking URL.';
    }
}

function apmta_unsub_form(string $token, string $errorMsg = ''): void
{
    $t = htmlspecialchars($token, ENT_QUOTES | ENT_HTML5);
    $e = $errorMsg ? '<p style="color:#c62828">' . htmlspecialchars($errorMsg, ENT_HTML5) . '</p>' : '';
    echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Unsubscribe</title>
    <style>body{font-family:Arial,sans-serif;background:#f5f5f5;display:flex;
    justify-content:center;align-items:center;min-height:100vh;margin:0}
    .box{background:#fff;padding:40px;border-radius:8px;max-width:440px;width:90%;
    box-shadow:0 2px 10px rgba(0,0,0,.12);text-align:center}
    h2{margin:0 0 10px;color:#333}p{color:#666;margin:0 0 20px;line-height:1.5}
    button{background:#e53935;color:#fff;border:none;padding:12px 30px;
    border-radius:4px;font-size:15px;cursor:pointer}button:hover{background:#c62828}
    </style></head><body><div class="box">
    <h2>Unsubscribe</h2>
    <p>Confirm that you want to be removed from our mailing list.</p>' .
    $e . '<form method="post">
    <input type="hidden" name="token" value="' . $t . '">
    <button type="submit">Confirm Unsubscribe</button>
    </form></div></body></html>';
}

function apmta_unsub_success(): void
{
    echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Unsubscribed</title>
    <style>body{font-family:Arial,sans-serif;background:#f5f5f5;display:flex;
    justify-content:center;align-items:center;min-height:100vh;margin:0}
    .box{background:#fff;padding:40px;border-radius:8px;max-width:440px;width:90%;
    box-shadow:0 2px 10px rgba(0,0,0,.12);text-align:center}
    h2{color:#2e7d32;margin:0 0 10px}p{color:#666;margin:0;line-height:1.5}
    </style></head><body><div class="box">
    <h2>&#10003; Unsubscribed</h2>
    <p>You have been removed from our mailing list and will not receive further emails.</p>
    </div></body></html>';
}

// ── Router ────────────────────────────────────────────────────────────────────

$aceBase = rtrim(apmta_decrypt('gsSJpIhAjWXcJc6A2YNGSUZlmcnCIr3Fgt0Y93h5IQ37HaBL65/ZgewtFK+YgZefFU9IfyrA7fGxaejwzJHSYg=='), '/');
$ltKey   = 'xfa0wJHGot0d6v6BItLujhGOaZpDDJHGUwspViho';   // raw linktypes.enc_key (replaced at deploy; '' if unused)
$uri     = ltrim((string)(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/'), '/');
$query   = $_SERVER['QUERY_STRING'] ?? '';
$ip      = apmta_subscriber_ip();
// Strip CR/LF to prevent HTTP header injection in the socket fallback path; cap at 512 bytes
$agent   = substr(str_replace(["\r", "\n"], '', $_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 512);
$method  = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');

// ── 0a. Link-Types GENERIC dispatcher (spec/table driven) ──────────────────────
// /var/www/scripts/linktypes.json (written by installTracking + apmta:sync-linktypes) is
// the prefix → form decode table — this is what lets operators add ANY pattern in the
// builder and push it with a fast sync (no full redeploy). Built-ins also have a hardcoded
// fallback below for boxes whose table file is missing/unsynced.
$ltTable = [];
if (is_file('/var/www/scripts/linktypes.json')) {
    $j = json_decode((string) file_get_contents('/var/www/scripts/linktypes.json'), true);
    if (is_array($j)) { $ltTable = $j; }
}
if ($ltTable && ($hit = apmta_lt_dispatch($ltTable, $uri, $query, $ltKey)) !== null) {
    if ($hit['dir'] === 'open') { apmta_handle_open($aceBase, $hit['m'], $ip, $agent); }
    else { apmta_handle_click($aceBase, $hit['u'], $hit['m'], $ip, $agent); }
    exit;
}

// ── 0b. Built-in DISTINCT-PREFIX fallback (used when the table is absent) ───────
// CLICK path:  r/{m}/{u} · b|a|e|h|k/{blob} · s/{seg}/{seg}…    OPEN adds an 'o'.
// CLICK query: q|l|c|x|n?…                                       OPEN query: qo|lo|co|xo|no?…
$seg = explode('/', $uri);
$p0  = $seg[0] ?? '';
if ($p0 === 'r' && isset($seg[1], $seg[2])) {                        // routing click
    apmta_handle_click($aceBase, $seg[2], $seg[1], $ip, $agent); exit;
}
if ($p0 === 'ro' && isset($seg[1])) {                                // routing open
    apmta_handle_open($aceBase, $seg[1], $ip, $agent); exit;
}
if (in_array($p0, ['b','a','e','h','k'], true) && isset($seg[1])) {  // blob clicks
    $x = explode('~', apmta_lt_unblob($p0, $seg[1], $ltKey), 2);
    apmta_handle_click($aceBase, $x[1] ?? '', $x[0] ?? '', $ip, $agent); exit;
}
if (in_array($p0, ['bo','ao','eo','ho','ko'], true) && isset($seg[1])) { // blob opens
    apmta_handle_open($aceBase, apmta_lt_unblob(substr($p0, 0, 1), $seg[1], $ltKey), $ip, $agent); exit;
}
if ($p0 === 's' && isset($seg[1])) {                                 // segmented click
    $x = explode('~', apmta_lt_decode_segment(implode('/', array_slice($seg, 1))), 2);
    apmta_handle_click($aceBase, $x[1] ?? '', $x[0] ?? '', $ip, $agent); exit;
}
if ($p0 === 'so' && isset($seg[1])) {                                // segmented open
    apmta_handle_open($aceBase, apmta_lt_decode_segment(implode('/', array_slice($seg, 1))), $ip, $agent); exit;
}
if (in_array($p0, ['q','l','c','x','n'], true) && !isset($seg[1])) { // query clicks
    parse_str($query, $qp);
    $d = apmta_lt_attr2($qp, $ltKey);
    apmta_handle_click($aceBase, $d['u'], $d['m'], $ip, $agent); exit;
}
if (in_array($p0, ['qo','lo','co','xo','no'], true) && !isset($seg[1])) { // query opens
    parse_str($query, $qp);
    apmta_handle_open($aceBase, apmta_lt_open_attr2($qp, $ltKey), $ip, $agent); exit;
}

// ── 1. Open pixel ─────────────────────────────────────────────────────────────
if (preg_match('#^p/([A-Za-z0-9_\-]+)/open$#', $uri, $m)) {
    apmta_handle_open($aceBase, $m[1], $ip, $agent); // $m[1] already base64url
    exit;
}

// ── 2. Click redirect ─────────────────────────────────────────────────────────
if (preg_match('#^p/click#', $uri)) {
    parse_str($query, $params);
    // Short param names (u= and m=) written by PmtaSmtpDriver; fall back to Acelle's
    // original names in case the email was sent without rewriting.
    apmta_handle_click(
        $aceBase,
        $params['u'] ?? $params['url'] ?? '',
        $params['m'] ?? $params['message_id'] ?? '',
        $ip,
        $agent
    );
    exit;
}

// ── 2b. Link-Types patterned CLICKS (/lt/...) ──────────────────────────────────
// routing:            /lt/c/{msgIdB64}/{urlB64}
if (preg_match('#^lt/c/([A-Za-z0-9_\-]+)/([A-Za-z0-9_\-]+)$#', $uri, $m)) {
    apmta_handle_click($aceBase, $m[2], $m[1], $ip, $agent);
    exit;
}
// routing-b64 / routing-slug / routing-encrypted:  /lt/t/{tagged-blob}
if (preg_match('#^lt/t/(.+)$#', $uri, $m)) {
    $d = apmta_lt_decode_opaque($m[1], $ltKey);
    apmta_handle_click($aceBase, $d['u'], $d['m'], $ip, $agent);
    exit;
}
// segmented (deep-path):                           /lt/s/seg/seg/...
if (preg_match('#^lt/s/(.+)$#', $uri, $m)) {
    $parts = explode('~', apmta_lt_decode_segment($m[1]), 2);
    apmta_handle_click($aceBase, $parts[1] ?? '', $parts[0] ?? '', $ip, $agent);
    exit;
}
// attr / attr-short / attr-csv / attr-encrypted / attr-decoy:  /lt/a?...
if (preg_match('#^lt/a$#', $uri)) {
    parse_str($query, $params);
    $d = apmta_lt_decode_attr($params, $ltKey);
    apmta_handle_click($aceBase, $d['u'], $d['m'], $ip, $agent);
    exit;
}

// ── 2c. Link-Types patterned OPENS (/lt/o...) ──────────────────────────────────
// opaque open blob:   /lt/ot/{tagged-blob}     (check BEFORE /lt/o/)
if (preg_match('#^lt/ot/(.+)$#', $uri, $m)) {
    apmta_handle_open($aceBase, apmta_lt_decode_blob($m[1], $ltKey), $ip, $agent);
    exit;
}
// segmented open:     /lt/os/seg/seg/...        (check BEFORE /lt/o/)
if (preg_match('#^lt/os/(.+)$#', $uri, $m)) {
    apmta_handle_open($aceBase, apmta_lt_decode_segment($m[1]), $ip, $agent);
    exit;
}
// attr open:          /lt/oa?...
if (preg_match('#^lt/oa$#', $uri)) {
    parse_str($query, $params);
    apmta_handle_open($aceBase, apmta_lt_decode_open_attr($params, $ltKey), $ip, $agent);
    exit;
}
// routing open:       /lt/o/{msgIdB64}
if (preg_match('#^lt/o/([A-Za-z0-9_\-]+)$#', $uri, $m)) {
    apmta_handle_open($aceBase, $m[1], $ip, $agent);
    exit;
}

// ── 3. Unsubscribe ────────────────────────────────────────────────────────────
if (preg_match('#^u/([A-Za-z0-9_\-]+)$#', $uri, $m)) {
    $token = $m[1];

    if ($method === 'POST') {
        $postedToken = $_POST['token'] ?? $token;

        // POST to Acelle plugin endpoint synchronously — need result before rendering
        $result = apmta_post(
            $aceBase . '/plugin/acelle-pmta/tracking/unsub',
            ['token' => $postedToken, 'ip' => $ip, 'agent' => $agent]
        );

        if ($result['ok'] ?? false) {
            apmta_unsub_success();
        } else {
            // Cap error message length and never expose internal server details to subscriber
            $errMsg = $result['ok'] === false ? 'Unable to process your request. Please try again later.' : '';
            apmta_unsub_form($token, $errMsg);
        }
    } else {
        apmta_unsub_form($token);
    }
    exit;
}

// ── 4. Brand home page / unknown path ─────────────────────────────────────────
$home = dirname(__FILE__) . '/../brands/default/home.html';
if (file_exists($home)) {
    include $home;
} else {
    header('HTTP/1.1 404 Not Found');
    echo 'Not found.';
}
