diff --git a/ai/LocalAIApi.php b/ai/LocalAIApi.php deleted file mode 100644 index d428248..0000000 --- a/ai/LocalAIApi.php +++ /dev/null @@ -1,493 +0,0 @@ - [ -// ['role' => 'system', 'content' => 'You are a helpful assistant.'], -// ['role' => 'user', 'content' => 'Tell me a bedtime story.'], -// ], -// ]); -// if (!empty($response['success'])) { -// // response['data'] contains full payload, e.g.: -// // { -// // "id": "resp_xxx", -// // "status": "completed", -// // "output": [ -// // {"type": "reasoning", "summary": []}, -// // {"type": "message", "content": [{"type": "output_text", "text": "Your final answer here."}]} -// // ] -// // } -// $decoded = LocalAIApi::decodeJsonFromResponse($response); // or inspect $response['data'] / extractText(...) -// } -// Poll settings override: -// LocalAIApi::createResponse($payload, ['poll_interval' => 5, 'poll_timeout' => 300]); - -class LocalAIApi -{ - /** @var array|null */ - private static ?array $configCache = null; - - /** - * Signature compatible with the OpenAI Responses API. - * - * @param array $params Request body (model, input, text, reasoning, metadata, etc.). - * @param array $options Extra options (timeout, verify_tls, headers, path, project_uuid). - * @return array{ - * success:bool, - * status?:int, - * data?:mixed, - * error?:string, - * response?:mixed, - * message?:string - * } - */ - public static function createResponse(array $params, array $options = []): array - { - $cfg = self::config(); - $payload = $params; - - if (empty($payload['input']) || !is_array($payload['input'])) { - return [ - 'success' => false, - 'error' => 'input_missing', - 'message' => 'Parameter "input" is required and must be an array.', - ]; - } - - if (!isset($payload['model']) || $payload['model'] === '') { - $payload['model'] = $cfg['default_model']; - } - - $initial = self::request($options['path'] ?? null, $payload, $options); - if (empty($initial['success'])) { - return $initial; - } - - // Async flow: if backend returns ai_request_id, poll status until ready - $data = $initial['data'] ?? null; - if (is_array($data) && isset($data['ai_request_id'])) { - $aiRequestId = $data['ai_request_id']; - $pollTimeout = isset($options['poll_timeout']) ? (int) $options['poll_timeout'] : 300; // seconds - $pollInterval = isset($options['poll_interval']) ? (int) $options['poll_interval'] : 5; // seconds - return self::awaitResponse($aiRequestId, [ - 'timeout' => $pollTimeout, - 'interval' => $pollInterval, - 'headers' => $options['headers'] ?? [], - 'timeout_per_call' => $options['timeout'] ?? null, - ]); - } - - return $initial; - } - - /** - * Snake_case alias for createResponse (matches the provided example). - * - * @param array $params - * @param array $options - * @return array - */ - public static function create_response(array $params, array $options = []): array - { - return self::createResponse($params, $options); - } - - /** - * Perform a raw request to the AI proxy. - * - * @param string $path Endpoint (may be an absolute URL). - * @param array $payload JSON payload. - * @param array $options Additional request options. - * @return array - */ - public static function request(?string $path = null, array $payload = [], array $options = []): array - { - $cfg = self::config(); - - $projectUuid = $cfg['project_uuid']; - if (empty($projectUuid)) { - return [ - 'success' => false, - 'error' => 'project_uuid_missing', - 'message' => 'PROJECT_UUID is not defined; aborting AI request.', - ]; - } - - $defaultPath = $cfg['responses_path'] ?? null; - $resolvedPath = $path ?? ($options['path'] ?? $defaultPath); - if (empty($resolvedPath)) { - return [ - 'success' => false, - 'error' => 'project_id_missing', - 'message' => 'PROJECT_ID is not defined; cannot resolve AI proxy endpoint.', - ]; - } - - $url = self::buildUrl($resolvedPath, $cfg['base_url']); - $baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30; - $timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout; - if ($timeout <= 0) { - $timeout = 30; - } - - $baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true; - $verifyTls = array_key_exists('verify_tls', $options) - ? (bool) $options['verify_tls'] - : $baseVerifyTls; - - $projectHeader = $cfg['project_header']; - - $headers = [ - 'Content-Type: application/json', - 'Accept: application/json', - ]; - $headers[] = $projectHeader . ': ' . $projectUuid; - if (!empty($options['headers']) && is_array($options['headers'])) { - foreach ($options['headers'] as $header) { - if (is_string($header) && $header !== '') { - $headers[] = $header; - } - } - } - - if (!empty($projectUuid) && !array_key_exists('project_uuid', $payload)) { - $payload['project_uuid'] = $projectUuid; - } - - $body = json_encode($payload, JSON_UNESCAPED_UNICODE); - if ($body === false) { - return [ - 'success' => false, - 'error' => 'json_encode_failed', - 'message' => 'Failed to encode request body to JSON.', - ]; - } - - return self::sendCurl($url, 'POST', $body, $headers, $timeout, $verifyTls); - } - - /** - * Poll AI request status until ready or timeout. - * - * @param int|string $aiRequestId - * @param array $options - * @return array - */ - public static function awaitResponse($aiRequestId, array $options = []): array - { - $cfg = self::config(); - - $timeout = isset($options['timeout']) ? (int) $options['timeout'] : 300; // seconds - $interval = isset($options['interval']) ? (int) $options['interval'] : 5; // seconds - if ($interval <= 0) { - $interval = 5; - } - $perCallTimeout = isset($options['timeout_per_call']) ? (int) $options['timeout_per_call'] : null; - - $deadline = time() + max($timeout, $interval); - $headers = $options['headers'] ?? []; - - while (true) { - $statusResp = self::fetchStatus($aiRequestId, [ - 'headers' => $headers, - 'timeout' => $perCallTimeout, - ]); - if (!empty($statusResp['success'])) { - $data = $statusResp['data'] ?? []; - if (is_array($data)) { - $statusValue = $data['status'] ?? null; - if ($statusValue === 'success') { - return [ - 'success' => true, - 'status' => 200, - 'data' => $data['response'] ?? $data, - ]; - } - if ($statusValue === 'failed') { - return [ - 'success' => false, - 'status' => 500, - 'error' => isset($data['error']) ? (string)$data['error'] : 'AI request failed', - 'data' => $data, - ]; - } - } - } else { - return $statusResp; - } - - if (time() >= $deadline) { - return [ - 'success' => false, - 'error' => 'timeout', - 'message' => 'Timed out waiting for AI response.', - ]; - } - sleep($interval); - } - } - - /** - * Fetch status for queued AI request. - * - * @param int|string $aiRequestId - * @param array $options - * @return array - */ - public static function fetchStatus($aiRequestId, array $options = []): array - { - $cfg = self::config(); - $projectUuid = $cfg['project_uuid']; - if (empty($projectUuid)) { - return [ - 'success' => false, - 'error' => 'project_uuid_missing', - 'message' => 'PROJECT_UUID is not defined; aborting status check.', - ]; - } - - $statusPath = self::resolveStatusPath($aiRequestId, $cfg); - $url = self::buildUrl($statusPath, $cfg['base_url']); - - $baseTimeout = isset($cfg['timeout']) ? (int) $cfg['timeout'] : 30; - $timeout = isset($options['timeout']) ? (int) $options['timeout'] : $baseTimeout; - if ($timeout <= 0) { - $timeout = 30; - } - - $baseVerifyTls = array_key_exists('verify_tls', $cfg) ? (bool) $cfg['verify_tls'] : true; - $verifyTls = array_key_exists('verify_tls', $options) - ? (bool) $options['verify_tls'] - : $baseVerifyTls; - - $projectHeader = $cfg['project_header']; - $headers = [ - 'Accept: application/json', - $projectHeader . ': ' . $projectUuid, - ]; - if (!empty($options['headers']) && is_array($options['headers'])) { - foreach ($options['headers'] as $header) { - if (is_string($header) && $header !== '') { - $headers[] = $header; - } - } - } - - return self::sendCurl($url, 'GET', null, $headers, $timeout, $verifyTls); - } - - /** - * Extract plain text from a Responses API payload. - * - * @param array $response Result of LocalAIApi::createResponse|request. - * @return string - */ - public static function extractText(array $response): string - { - $payload = $response['data'] ?? $response; - if (!is_array($payload)) { - return ''; - } - - if (!empty($payload['output']) && is_array($payload['output'])) { - $combined = ''; - foreach ($payload['output'] as $item) { - if (!isset($item['content']) || !is_array($item['content'])) { - continue; - } - foreach ($item['content'] as $block) { - if (is_array($block) && ($block['type'] ?? '') === 'output_text' && !empty($block['text'])) { - $combined .= $block['text']; - } - } - } - if ($combined !== '') { - return $combined; - } - } - - if (!empty($payload['choices'][0]['message']['content'])) { - return (string) $payload['choices'][0]['message']['content']; - } - - return ''; - } - - /** - * Attempt to decode JSON emitted by the model (handles markdown fences). - * - * @param array $response - * @return array|null - */ - public static function decodeJsonFromResponse(array $response): ?array - { - $text = self::extractText($response); - if ($text === '') { - return null; - } - - $decoded = json_decode($text, true); - if (is_array($decoded)) { - return $decoded; - } - - $stripped = preg_replace('/^```json|```$/m', '', trim($text)); - if ($stripped !== null && $stripped !== $text) { - $decoded = json_decode($stripped, true); - if (is_array($decoded)) { - return $decoded; - } - } - - return null; - } - - /** - * Load configuration from ai/config.php. - * - * @return array - */ - private static function config(): array - { - if (self::$configCache === null) { - $configPath = __DIR__ . '/config.php'; - if (!file_exists($configPath)) { - throw new RuntimeException('AI config file not found: ai/config.php'); - } - $cfg = require $configPath; - if (!is_array($cfg)) { - throw new RuntimeException('Invalid AI config format: expected array'); - } - self::$configCache = $cfg; - } - - return self::$configCache; - } - - /** - * Build an absolute URL from base_url and a path. - */ - private static function buildUrl(string $path, string $baseUrl): string - { - $trimmed = trim($path); - if ($trimmed === '') { - return $baseUrl; - } - if (str_starts_with($trimmed, 'http://') || str_starts_with($trimmed, 'https://')) { - return $trimmed; - } - if ($trimmed[0] === '/') { - return $baseUrl . $trimmed; - } - return $baseUrl . '/' . $trimmed; - } - - /** - * Resolve status path based on configured responses_path and ai_request_id. - * - * @param int|string $aiRequestId - * @param array $cfg - * @return string - */ - private static function resolveStatusPath($aiRequestId, array $cfg): string - { - $basePath = $cfg['responses_path'] ?? ''; - $trimmed = rtrim($basePath, '/'); - if ($trimmed === '') { - return '/ai-request/' . rawurlencode((string)$aiRequestId) . '/status'; - } - if (substr($trimmed, -11) !== '/ai-request') { - $trimmed .= '/ai-request'; - } - return $trimmed . '/' . rawurlencode((string)$aiRequestId) . '/status'; - } - - /** - * Shared CURL sender for GET/POST requests. - * - * @param string $url - * @param string $method - * @param string|null $body - * @param array $headers - * @param int $timeout - * @param bool $verifyTls - * @return array - */ - private static function sendCurl(string $url, string $method, ?string $body, array $headers, int $timeout, bool $verifyTls): array - { - if (!function_exists('curl_init')) { - return [ - 'success' => false, - 'error' => 'curl_missing', - 'message' => 'PHP cURL extension is missing. Install or enable it on the VM.', - ]; - } - - $ch = curl_init($url); - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifyTls); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyTls ? 2 : 0); - curl_setopt($ch, CURLOPT_FAILONERROR, false); - - $upper = strtoupper($method); - if ($upper === 'POST') { - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $body ?? ''); - } else { - curl_setopt($ch, CURLOPT_HTTPGET, true); - } - - $responseBody = curl_exec($ch); - if ($responseBody === false) { - $error = curl_error($ch) ?: 'Unknown cURL error'; - curl_close($ch); - return [ - 'success' => false, - 'error' => 'curl_error', - 'message' => $error, - ]; - } - - $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - $decoded = null; - if ($responseBody !== '' && $responseBody !== null) { - $decoded = json_decode($responseBody, true); - if (json_last_error() !== JSON_ERROR_NONE) { - $decoded = null; - } - } - - if ($status >= 200 && $status < 300) { - return [ - 'success' => true, - 'status' => $status, - 'data' => $decoded ?? $responseBody, - ]; - } - - $errorMessage = 'AI proxy request failed'; - if (is_array($decoded)) { - $errorMessage = $decoded['error'] ?? $decoded['message'] ?? $errorMessage; - } elseif (is_string($responseBody) && $responseBody !== '') { - $errorMessage = $responseBody; - } - - return [ - 'success' => false, - 'status' => $status, - 'error' => $errorMessage, - 'response' => $decoded ?? $responseBody, - ]; - } -} - -// Legacy alias for backward compatibility with the previous class name. -if (!class_exists('OpenAIService')) { - class_alias(LocalAIApi::class, 'OpenAIService'); -} diff --git a/ai/config.php b/ai/config.php deleted file mode 100644 index c890698..0000000 --- a/ai/config.php +++ /dev/null @@ -1,52 +0,0 @@ - $baseUrl, - 'responses_path' => $responsesPath, - 'project_id' => $projectId, - 'project_uuid' => $projectUuid, - 'project_header' => 'project-uuid', - 'default_model' => 'gpt-5-mini', - 'timeout' => 30, - 'verify_tls' => true, -]; diff --git a/db/config.php b/db/config.php deleted file mode 100644 index a73fca0..0000000 --- a/db/config.php +++ /dev/null @@ -1,17 +0,0 @@ - PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); - } - return $pdo; -} diff --git a/index.php b/index.php deleted file mode 100644 index 7205f3d..0000000 --- a/index.php +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… -
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
-
-
- Page updated: (UTC) -
- - diff --git a/mail/MailService.php b/mail/MailService.php deleted file mode 100644 index d801067..0000000 --- a/mail/MailService.php +++ /dev/null @@ -1,235 +0,0 @@ - false, 'error' => 'PHPMailer not available' ]; - } - - $mail = new PHPMailer\PHPMailer\PHPMailer(true); - try { - $mail->isSMTP(); - $mail->Host = $cfg['smtp_host'] ?? ''; - $mail->Port = (int)($cfg['smtp_port'] ?? 587); - $secure = $cfg['smtp_secure'] ?? 'tls'; - if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS; - elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; - else $mail->SMTPSecure = false; - $mail->SMTPAuth = true; - $mail->Username = $cfg['smtp_user'] ?? ''; - $mail->Password = $cfg['smtp_pass'] ?? ''; - - $fromEmail = $opts['from_email'] ?? ($cfg['from_email'] ?? 'no-reply@localhost'); - $fromName = $opts['from_name'] ?? ($cfg['from_name'] ?? 'App'); - $mail->setFrom($fromEmail, $fromName); - if (!empty($opts['reply_to']) && filter_var($opts['reply_to'], FILTER_VALIDATE_EMAIL)) { - $mail->addReplyTo($opts['reply_to']); - } elseif (!empty($cfg['reply_to'])) { - $mail->addReplyTo($cfg['reply_to']); - } - - // Recipients - $toList = []; - if ($to) { - if (is_string($to)) $toList = array_map('trim', explode(',', $to)); - elseif (is_array($to)) $toList = $to; - } elseif (!empty(getenv('MAIL_TO'))) { - $toList = array_map('trim', explode(',', getenv('MAIL_TO'))); - } - $added = 0; - foreach ($toList as $addr) { - if (filter_var($addr, FILTER_VALIDATE_EMAIL)) { $mail->addAddress($addr); $added++; } - } - if ($added === 0) { - return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ]; - } - - foreach ((array)($opts['cc'] ?? []) as $cc) { if (filter_var($cc, FILTER_VALIDATE_EMAIL)) $mail->addCC($cc); } - foreach ((array)($opts['bcc'] ?? []) as $bcc){ if (filter_var($bcc, FILTER_VALIDATE_EMAIL)) $mail->addBCC($bcc); } - - // Optional DKIM - if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) { - $mail->DKIM_domain = $cfg['dkim_domain']; - $mail->DKIM_selector = $cfg['dkim_selector']; - $mail->DKIM_private = $cfg['dkim_private_key_path']; - } - - $mail->isHTML(true); - $mail->Subject = $subject; - $mail->Body = $htmlBody; - $mail->AltBody = $textBody ?? strip_tags($htmlBody); - $ok = $mail->send(); - return [ 'success' => $ok ]; - } catch (\Throwable $e) { - return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ]; - } - } - private static function loadConfig(): array - { - $configPath = __DIR__ . '/config.php'; - if (!file_exists($configPath)) { - throw new \RuntimeException('Mail config not found. Copy mail/config.sample.php to mail/config.php and fill in credentials.'); - } - $cfg = require $configPath; - if (!is_array($cfg)) { - throw new \RuntimeException('Invalid mail config format: expected array'); - } - return $cfg; - } - - // Send a contact message - // $to can be: a single email string, a comma-separated list, an array of emails, or null (fallback to MAIL_TO/MAIL_FROM) - public static function sendContactMessage(string $name, string $email, string $message, $to = null, string $subject = 'New contact form') - { - $cfg = self::loadConfig(); - - // Try Composer autoload if available (for PHPMailer) - $autoload = __DIR__ . '/../vendor/autoload.php'; - if (file_exists($autoload)) { - require_once $autoload; - } - // Fallback to system-wide PHPMailer (installed via apt: libphp-phpmailer) - if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { - // Debian/Ubuntu package layout (libphp-phpmailer) - @require_once 'libphp-phpmailer/autoload.php'; - if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { - @require_once 'libphp-phpmailer/src/Exception.php'; - @require_once 'libphp-phpmailer/src/SMTP.php'; - @require_once 'libphp-phpmailer/src/PHPMailer.php'; - } - // Alternative layout (older PHPMailer package names) - if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { - @require_once 'PHPMailer/src/Exception.php'; - @require_once 'PHPMailer/src/SMTP.php'; - @require_once 'PHPMailer/src/PHPMailer.php'; - } - if (!class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { - @require_once 'PHPMailer/Exception.php'; - @require_once 'PHPMailer/SMTP.php'; - @require_once 'PHPMailer/PHPMailer.php'; - } - } - - $transport = $cfg['transport'] ?? 'smtp'; - if ($transport === 'smtp' && class_exists('PHPMailer\\PHPMailer\\PHPMailer')) { - return self::sendViaPHPMailer($cfg, $name, $email, $message, $to, $subject); - } - - // Fallback: attempt native mail() — works only if MTA is configured on the VM - return self::sendViaNativeMail($cfg, $name, $email, $message, $to, $subject); - } - - private static function sendViaPHPMailer(array $cfg, string $name, string $email, string $body, $to, string $subject) - { - $mail = new PHPMailer\PHPMailer\PHPMailer(true); - try { - $mail->isSMTP(); - $mail->Host = $cfg['smtp_host'] ?? ''; - $mail->Port = (int)($cfg['smtp_port'] ?? 587); - $secure = $cfg['smtp_secure'] ?? 'tls'; - if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS; - elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; - else $mail->SMTPSecure = false; - $mail->SMTPAuth = true; - $mail->Username = $cfg['smtp_user'] ?? ''; - $mail->Password = $cfg['smtp_pass'] ?? ''; - - $fromEmail = $cfg['from_email'] ?? 'no-reply@localhost'; - $fromName = $cfg['from_name'] ?? 'App'; - $mail->setFrom($fromEmail, $fromName); - - // Use Reply-To for the user's email to avoid spoofing From - if (filter_var($email, FILTER_VALIDATE_EMAIL)) { - $mail->addReplyTo($email, $name ?: $email); - } - if (!empty($cfg['reply_to'])) { - $mail->addReplyTo($cfg['reply_to']); - } - - // Destination: prefer dynamic recipients ($to), fallback to MAIL_TO; no silent FROM fallback - $toList = []; - if ($to) { - if (is_string($to)) { - // allow comma-separated list - $toList = array_map('trim', explode(',', $to)); - } elseif (is_array($to)) { - $toList = $to; - } - } elseif (!empty(getenv('MAIL_TO'))) { - $toList = array_map('trim', explode(',', getenv('MAIL_TO'))); - } - $added = 0; - foreach ($toList as $addr) { - if (filter_var($addr, FILTER_VALIDATE_EMAIL)) { - $mail->addAddress($addr); - $added++; - } - } - if ($added === 0) { - return [ 'success' => false, 'error' => 'No recipients defined (set MAIL_TO or pass $to)' ]; - } - - // DKIM (optional) - if (!empty($cfg['dkim_domain']) && !empty($cfg['dkim_selector']) && !empty($cfg['dkim_private_key_path'])) { - $mail->DKIM_domain = $cfg['dkim_domain']; - $mail->DKIM_selector = $cfg['dkim_selector']; - $mail->DKIM_private = $cfg['dkim_private_key_path']; - } - - $mail->isHTML(true); - $mail->Subject = $subject; - $safeName = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - $safeEmail = htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - $safeBody = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')); - $mail->Body = "

Name: {$safeName}

Email: {$safeEmail}


{$safeBody}"; - $mail->AltBody = "Name: {$name}\nEmail: {$email}\n\n{$body}"; - - $ok = $mail->send(); - return [ 'success' => $ok ]; - } catch (\Throwable $e) { - return [ 'success' => false, 'error' => 'PHPMailer error: ' . $e->getMessage() ]; - } - } - - private static function sendViaNativeMail(array $cfg, string $name, string $email, string $body, $to, string $subject) - { - $opts = ['reply_to' => $email]; - $html = nl2br(htmlspecialchars($body, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')); - return self::sendMail($to, $subject, $html, $body, $opts); - } -} diff --git a/mail/config.php b/mail/config.php deleted file mode 100644 index 626cca1..0000000 --- a/mail/config.php +++ /dev/null @@ -1,76 +0,0 @@ - config array for MailService. - -function env_val(string $key, $default = null) { - $v = getenv($key); - return ($v === false || $v === null || $v === '') ? $default : $v; -} - -// Fallback: if critical vars are missing from process env, try to parse executor/.env -// This helps in web/Apache contexts where .env is not exported. -// Supports simple KEY=VALUE lines; ignores quotes and comments. -function load_dotenv_if_needed(array $keys): void { - $missing = array_filter($keys, fn($k) => getenv($k) === false || getenv($k) === ''); - if (empty($missing)) return; - static $loaded = false; - if ($loaded) return; - $envPath = realpath(__DIR__ . '/../../.env'); // executor/.env - if ($envPath && is_readable($envPath)) { - $lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; - foreach ($lines as $line) { - if ($line[0] === '#' || trim($line) === '') continue; - if (!str_contains($line, '=')) continue; - [$k, $v] = array_map('trim', explode('=', $line, 2)); - // Strip potential surrounding quotes - $v = trim($v, "\"' "); - // Do not override existing env - if ($k !== '' && (getenv($k) === false || getenv($k) === '')) { - putenv("{$k}={$v}"); - } - } - $loaded = true; - } -} - -load_dotenv_if_needed([ - 'MAIL_TRANSPORT','SMTP_HOST','SMTP_PORT','SMTP_SECURE','SMTP_USER','SMTP_PASS', - 'MAIL_FROM','MAIL_FROM_NAME','MAIL_REPLY_TO','MAIL_TO', - 'DKIM_DOMAIN','DKIM_SELECTOR','DKIM_PRIVATE_KEY_PATH' -]); - -$transport = env_val('MAIL_TRANSPORT', 'smtp'); -$smtp_host = env_val('SMTP_HOST'); -$smtp_port = (int) env_val('SMTP_PORT', 587); -$smtp_secure = env_val('SMTP_SECURE', 'tls'); // tls | ssl | null -$smtp_user = env_val('SMTP_USER'); -$smtp_pass = env_val('SMTP_PASS'); - -$from_email = env_val('MAIL_FROM', 'no-reply@localhost'); -$from_name = env_val('MAIL_FROM_NAME', 'App'); -$reply_to = env_val('MAIL_REPLY_TO'); - -$dkim_domain = env_val('DKIM_DOMAIN'); -$dkim_selector = env_val('DKIM_SELECTOR'); -$dkim_private_key_path = env_val('DKIM_PRIVATE_KEY_PATH'); - -return [ - 'transport' => $transport, - - // SMTP - 'smtp_host' => $smtp_host, - 'smtp_port' => $smtp_port, - 'smtp_secure' => $smtp_secure, - 'smtp_user' => $smtp_user, - 'smtp_pass' => $smtp_pass, - - // From / Reply-To - 'from_email' => $from_email, - 'from_name' => $from_name, - 'reply_to' => $reply_to, - - // DKIM (optional) - 'dkim_domain' => $dkim_domain, - 'dkim_selector' => $dkim_selector, - 'dkim_private_key_path' => $dkim_private_key_path, -];