From 577ca93381928ab07c2fa7b3ebfc3f253c698ddb Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Tue, 4 Nov 2025 12:13:19 +0000 Subject: [PATCH] Initial version --- .gitignore | 3 + .htaccess | 18 ++++ db/config.php | 17 ++++ index.php | 150 +++++++++++++++++++++++++++ mail/MailService.php | 235 +++++++++++++++++++++++++++++++++++++++++++ mail/config.php | 76 ++++++++++++++ 6 files changed, 499 insertions(+) create mode 100644 .gitignore create mode 100644 .htaccess create mode 100644 db/config.php create mode 100644 index.php create mode 100644 mail/MailService.php create mode 100644 mail/config.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e427ff3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*/node_modules/ +*/build/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..e2bbc23 --- /dev/null +++ b/.htaccess @@ -0,0 +1,18 @@ +DirectoryIndex index.php index.html +Options -Indexes +Options -MultiViews + +RewriteEngine On + +# 0) Serve existing files/directories as-is +RewriteCond %{REQUEST_FILENAME} -f [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^ - [L] + +# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists) +RewriteCond %{REQUEST_FILENAME}.php -f +RewriteRule ^(.+?)/?$ $1.php [L] + +# 2) Optional: strip trailing slash for non-directories (keeps .php links working) +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.+)/$ $1 [R=301,L] diff --git a/db/config.php b/db/config.php new file mode 100644 index 0000000..f12ebaf --- /dev/null +++ b/db/config.php @@ -0,0 +1,17 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]); + } + return $pdo; +} diff --git a/index.php b/index.php new file mode 100644 index 0000000..7205f3d --- /dev/null +++ b/index.php @@ -0,0 +1,150 @@ + + + + + + + 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

+
+
+ + + diff --git a/mail/MailService.php b/mail/MailService.php new file mode 100644 index 0000000..d801067 --- /dev/null +++ b/mail/MailService.php @@ -0,0 +1,235 @@ + 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 new file mode 100644 index 0000000..626cca1 --- /dev/null +++ b/mail/config.php @@ -0,0 +1,76 @@ + 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, +];