379 lines
10 KiB
PHP
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;
|
|
}
|