40061-vm/includes/app.php
Flatlogic Bot f9536ee19b 01
2026-05-24 07:14:49 +00:00

379 lines
10 KiB
PHP

<?php
declare(strict_types=1);
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/../db/config.php';
function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
function project_name(): string
{
$name = $_SERVER['PROJECT_NAME'] ?? getenv('PROJECT_NAME') ?: 'Local POP3 Webmail';
return trim((string) $name);
}
function project_description_default(string $fallback): string
{
$description = $_SERVER['PROJECT_DESCRIPTION'] ?? getenv('PROJECT_DESCRIPTION') ?: $fallback;
return trim((string) $description);
}
function project_image_url(): string
{
$image = $_SERVER['PROJECT_IMAGE_URL'] ?? getenv('PROJECT_IMAGE_URL') ?: '';
return trim((string) $image);
}
function asset_version(string $relativePath): string
{
$fullPath = dirname(__DIR__) . '/' . ltrim($relativePath, '/');
return (string) (file_exists($fullPath) ? filemtime($fullPath) : time());
}
function app_db(): ?PDO
{
static $pdo = null;
static $attempted = false;
if ($attempted) {
return $pdo;
}
$attempted = true;
try {
$pdo = db();
} catch (Throwable $exception) {
$pdo = null;
$GLOBALS['APP_DB_ERROR'] = $exception->getMessage();
}
return $pdo;
}
function app_db_error(): ?string
{
return $GLOBALS['APP_DB_ERROR'] ?? null;
}
function ensure_mail_schema(): bool
{
static $ensured = false;
if ($ensured) {
return app_db() instanceof PDO;
}
$ensured = true;
$pdo = app_db();
if (!$pdo) {
return false;
}
$migrationFile = __DIR__ . '/../db/migrations/20260524_create_mail_accounts.sql';
try {
$sql = file_get_contents($migrationFile);
if ($sql === false) {
throw new RuntimeException('Unable to read the mailbox migration file.');
}
$pdo->exec($sql);
return true;
} catch (Throwable $exception) {
$GLOBALS['APP_DB_ERROR'] = $exception->getMessage();
return false;
}
}
function app_boot(): void
{
ensure_mail_schema();
}
function db_ready(): bool
{
return app_db() instanceof PDO;
}
function flash(string $type, string $message): void
{
$_SESSION['flash'] = [
'type' => $type,
'message' => $message,
];
}
function pull_flash(): ?array
{
if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) {
return null;
}
$flash = $_SESSION['flash'];
unset($_SESSION['flash']);
return $flash;
}
function mail_cipher_key(): string
{
return hash('sha256', DB_HOST . '|' . DB_NAME . '|' . DB_USER . '|' . DB_PASS, true);
}
function encrypt_secret(string $plaintext): string
{
$cipher = 'aes-256-cbc';
$ivLength = openssl_cipher_iv_length($cipher);
$iv = random_bytes($ivLength);
$encrypted = openssl_encrypt($plaintext, $cipher, mail_cipher_key(), OPENSSL_RAW_DATA, $iv);
if ($encrypted === false) {
throw new RuntimeException('Unable to securely store the POP3 password.');
}
return base64_encode($iv . $encrypted);
}
function decrypt_secret(string $ciphertext): string
{
$decoded = base64_decode($ciphertext, true);
if ($decoded === false) {
return '';
}
$cipher = 'aes-256-cbc';
$ivLength = openssl_cipher_iv_length($cipher);
$iv = substr($decoded, 0, $ivLength);
$payload = substr($decoded, $ivLength);
$decrypted = openssl_decrypt($payload, $cipher, mail_cipher_key(), OPENSSL_RAW_DATA, $iv);
return $decrypted === false ? '' : $decrypted;
}
function default_mail_account_input(): array
{
return [
'label' => '',
'email_address' => '',
'pop3_host' => '127.0.0.1',
'pop3_port' => 110,
'security_mode' => 'plain',
'username' => '',
'password' => '',
'sync_limit' => 15,
'leave_on_server' => 1,
];
}
function validate_mail_account_input(array $input): array
{
$clean = [
'label' => trim((string) ($input['label'] ?? '')),
'email_address' => trim((string) ($input['email_address'] ?? '')),
'pop3_host' => trim((string) ($input['pop3_host'] ?? '')),
'pop3_port' => (int) ($input['pop3_port'] ?? 110),
'security_mode' => in_array(($input['security_mode'] ?? 'plain'), ['plain', 'ssl'], true) ? (string) $input['security_mode'] : 'plain',
'username' => trim((string) ($input['username'] ?? '')),
'password' => trim((string) ($input['password'] ?? '')),
'sync_limit' => (int) ($input['sync_limit'] ?? 15),
'leave_on_server' => isset($input['leave_on_server']) ? 1 : 0,
];
$errors = [];
if ($clean['label'] === '' || strlen($clean['label']) < 2) {
$errors['label'] = 'Unesite naziv mailboxa (najmanje 2 znaka).';
}
if ($clean['email_address'] !== '' && !filter_var($clean['email_address'], FILTER_VALIDATE_EMAIL)) {
$errors['email_address'] = 'Email adresa nije ispravna.';
}
if ($clean['pop3_host'] === '' || strlen($clean['pop3_host']) < 2) {
$errors['pop3_host'] = 'POP3 host je obavezan.';
}
if ($clean['pop3_port'] < 1 || $clean['pop3_port'] > 65535) {
$errors['pop3_port'] = 'POP3 port mora biti između 1 i 65535.';
}
if ($clean['username'] === '') {
$errors['username'] = 'Korisničko ime je obavezno.';
}
if ($clean['password'] === '') {
$errors['password'] = 'Lozinka je obavezna.';
}
if ($clean['sync_limit'] < 5 || $clean['sync_limit'] > 50) {
$errors['sync_limit'] = 'Prikaži između 5 i 50 poruka po sinkronizaciji.';
}
return [$clean, $errors];
}
function save_mail_account(array $data): int
{
$pdo = app_db();
if (!$pdo) {
throw new RuntimeException('Baza trenutno nije dostupna.');
}
$statement = $pdo->prepare(
'INSERT INTO mail_accounts (label, email_address, pop3_host, pop3_port, security_mode, username, password_ciphertext, sync_limit, leave_on_server, last_status)
VALUES (:label, :email_address, :pop3_host, :pop3_port, :security_mode, :username, :password_ciphertext, :sync_limit, :leave_on_server, :last_status)'
);
$statement->bindValue(':label', $data['label']);
$statement->bindValue(':email_address', $data['email_address'] !== '' ? $data['email_address'] : null, PDO::PARAM_STR);
$statement->bindValue(':pop3_host', $data['pop3_host']);
$statement->bindValue(':pop3_port', (int) $data['pop3_port'], PDO::PARAM_INT);
$statement->bindValue(':security_mode', $data['security_mode']);
$statement->bindValue(':username', $data['username']);
$statement->bindValue(':password_ciphertext', encrypt_secret($data['password']));
$statement->bindValue(':sync_limit', (int) $data['sync_limit'], PDO::PARAM_INT);
$statement->bindValue(':leave_on_server', (int) $data['leave_on_server'], PDO::PARAM_INT);
$statement->bindValue(':last_status', 'Ready to connect');
$statement->execute();
return (int) $pdo->lastInsertId();
}
function get_mail_accounts(): array
{
$pdo = app_db();
if (!$pdo) {
return [];
}
$statement = $pdo->prepare(
'SELECT id, label, email_address, pop3_host, pop3_port, security_mode, username, sync_limit, leave_on_server, last_status, last_message_count, last_sync_at, created_at, updated_at
FROM mail_accounts
ORDER BY created_at DESC, id DESC'
);
$statement->execute();
return $statement->fetchAll() ?: [];
}
function find_mail_account(int $id): ?array
{
$pdo = app_db();
if (!$pdo) {
return null;
}
$statement = $pdo->prepare(
'SELECT id, label, email_address, pop3_host, pop3_port, security_mode, username, password_ciphertext, sync_limit, leave_on_server, last_status, last_message_count, last_sync_at, created_at, updated_at
FROM mail_accounts
WHERE id = :id
LIMIT 1'
);
$statement->bindValue(':id', $id, PDO::PARAM_INT);
$statement->execute();
$account = $statement->fetch();
return $account ?: null;
}
function update_mail_account_sync(int $id, string $status, int $messageCount): void
{
$pdo = app_db();
if (!$pdo) {
return;
}
$statement = $pdo->prepare(
'UPDATE mail_accounts
SET last_status = :last_status,
last_message_count = :last_message_count,
last_sync_at = NOW()
WHERE id = :id'
);
$statement->bindValue(':last_status', substr($status, 0, 255));
$statement->bindValue(':last_message_count', max(0, $messageCount), PDO::PARAM_INT);
$statement->bindValue(':id', $id, PDO::PARAM_INT);
$statement->execute();
}
function format_datetime(?string $value, string $fallback = 'Not yet'): string
{
if (!$value) {
return $fallback;
}
try {
return (new DateTimeImmutable($value))->format('M j, Y · H:i');
} catch (Throwable $exception) {
return $fallback;
}
}
function status_tone(?string $status): string
{
$value = strtolower(trim((string) $status));
if ($value === '') {
return 'status-idle';
}
if (str_contains($value, 'fail') || str_contains($value, 'error')) {
return 'status-danger';
}
if (str_contains($value, 'empty')) {
return 'status-warning';
}
if (str_contains($value, 'connected') || str_contains($value, 'ready')) {
return 'status-success';
}
return 'status-idle';
}
function security_label(string $mode): string
{
return $mode === 'ssl' ? 'SSL/TLS' : 'Plain';
}
function truncate_text(string $text, int $length = 160): string
{
$text = trim(preg_replace('/\s+/', ' ', $text) ?? $text);
if ($text === '') {
return '';
}
if (function_exists('iconv_strlen') && function_exists('iconv_substr')) {
$currentLength = iconv_strlen($text, 'UTF-8');
if ($currentLength !== false && $currentLength > $length) {
return rtrim((string) iconv_substr($text, 0, $length, 'UTF-8')) . '…';
}
}
return strlen($text) > $length ? rtrim(substr($text, 0, $length)) . '…' : $text;
}