334 lines
20 KiB
PHP
334 lines
20 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/includes/app.php';
|
|
|
|
app_boot();
|
|
|
|
$defaults = default_mail_account_input();
|
|
$formData = $defaults;
|
|
$errors = [];
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'save_account') {
|
|
[$formData, $errors] = validate_mail_account_input($_POST);
|
|
|
|
if (empty($errors)) {
|
|
try {
|
|
$accountId = save_mail_account($formData);
|
|
flash('success', 'Mailbox je spremljen. Otvaram live inbox pregled.');
|
|
header('Location: mailbox.php?id=' . $accountId);
|
|
exit;
|
|
} catch (Throwable $exception) {
|
|
$errors['form'] = 'Spremanje nije uspjelo. Provjeri bazu i pokušaj ponovno.';
|
|
}
|
|
}
|
|
}
|
|
|
|
$accounts = get_mail_accounts();
|
|
$flash = pull_flash();
|
|
$dbReady = db_ready();
|
|
$dbError = app_db_error();
|
|
$latestSync = 'Not yet';
|
|
$syncedAccounts = 0;
|
|
|
|
foreach ($accounts as $account) {
|
|
if (!empty($account['last_sync_at'])) {
|
|
$syncedAccounts++;
|
|
if ($latestSync === 'Not yet' || strtotime((string) $account['last_sync_at']) > strtotime((string) $latestSync)) {
|
|
$latestSync = (string) $account['last_sync_at'];
|
|
}
|
|
}
|
|
}
|
|
|
|
$pageTitle = project_name() . ' — POP3 mailbox dashboard';
|
|
$projectBaseDescription = trim((string) ($_SERVER['PROJECT_DESCRIPTION'] ?? getenv('PROJECT_DESCRIPTION') ?: ''));
|
|
$pageDescription = $projectBaseDescription !== ''
|
|
? $projectBaseDescription . ' — Dashboard for POP3 mailbox setup, MySQL account storage, and live inbox access.'
|
|
: 'Configure local POP3 mailboxes, store connection settings in MySQL, and open a clean inbox view from one lightweight PHP interface.';
|
|
$projectDescription = $projectBaseDescription !== '' ? $projectBaseDescription : $pageDescription;
|
|
$projectImageUrl = project_image_url();
|
|
?>
|
|
<!doctype html>
|
|
<html lang="hr">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title><?= h($pageTitle) ?></title>
|
|
<meta name="description" content="<?= h($pageDescription) ?>">
|
|
<?php if ($projectDescription): ?>
|
|
<meta property="og:description" content="<?= h($projectDescription) ?>">
|
|
<meta property="twitter:description" content="<?= h($projectDescription) ?>">
|
|
<?php endif; ?>
|
|
<?php if ($projectImageUrl): ?>
|
|
<meta property="og:image" content="<?= h($projectImageUrl) ?>">
|
|
<meta property="twitter:image" content="<?= h($projectImageUrl) ?>">
|
|
<?php endif; ?>
|
|
<meta property="og:title" content="<?= h($pageTitle) ?>">
|
|
<meta property="twitter:title" content="<?= h($pageTitle) ?>">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="assets/css/custom.css?v=<?= h(asset_version('assets/css/custom.css')) ?>">
|
|
</head>
|
|
<body>
|
|
<?php if ($flash): ?>
|
|
<div class="toast-shell">
|
|
<div class="toast align-items-center border-0" role="status" aria-live="polite" aria-atomic="true">
|
|
<div class="d-flex">
|
|
<div class="toast-body"><?= h($flash['message']) ?></div>
|
|
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
<div class="container app-shell py-4 py-lg-5">
|
|
<nav class="navbar app-nav navbar-expand-lg">
|
|
<div class="container-fluid px-0">
|
|
<a class="navbar-brand d-flex align-items-center gap-3 m-0" href="index.php">
|
|
<span class="brand-mark">WM</span>
|
|
<span class="brand-copy">
|
|
<strong><?= h(project_name()) ?></strong>
|
|
<small>Local POP3 workspace</small>
|
|
</span>
|
|
</a>
|
|
<div class="d-flex flex-wrap align-items-center gap-2 ms-auto">
|
|
<a class="btn btn-sm btn-outline-secondary" href="#setup">Dodaj račun</a>
|
|
<a class="btn btn-sm btn-outline-secondary" href="#accounts">Računi</a>
|
|
<a class="btn btn-sm btn-outline-secondary" href="xampp-setup.php">XAMPP setup</a>
|
|
<a class="btn btn-sm btn-outline-secondary" href="healthz.php" target="_blank" rel="noopener">Healthz</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<header class="hero-grid">
|
|
<section class="hero-card">
|
|
<div class="page-eyebrow mb-2">Initial MVP slice</div>
|
|
<h1 class="hero-title">Dodaj POP3 mailbox i odmah otvori inbox iz preglednika.</h1>
|
|
<p class="section-subtitle">Ova prva verzija pokriva najbitniji tok: spremi POP3 postavke u MySQL, otvori live pregled inboxa i pročitaj poruku bez izlaska iz aplikacije. Dizajn je namjerno čist i lagan za lokalni XAMPP setup.</p>
|
|
<div class="hero-actions">
|
|
<a class="btn btn-primary" href="#setup">Konfiguriraj mailbox</a>
|
|
<a class="btn btn-outline-secondary" href="#accounts">Pogledaj spremljene račune</a>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2 mt-3">
|
|
<span class="inline-note">POP3 read workflow</span>
|
|
<span class="inline-note">MySQL account storage</span>
|
|
<span class="inline-note">Inbox detail view</span>
|
|
</div>
|
|
</section>
|
|
<aside class="metrics-grid">
|
|
<div class="metric-card">
|
|
<div class="metric-label">Configured accounts</div>
|
|
<div class="metric-value"><?= h((string) count($accounts)) ?></div>
|
|
<div class="metric-hint">Broj mailboxa spremljenih u lokalnoj bazi.</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-label">Successful syncs</div>
|
|
<div class="metric-value"><?= h((string) $syncedAccounts) ?></div>
|
|
<div class="metric-hint">Računi koji su već otvoreni kroz inbox ekran.</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-label">Environment</div>
|
|
<div class="metric-value" style="font-size:1.1rem;">PHP <?= h(PHP_VERSION) ?></div>
|
|
<div class="metric-hint">Vrijeme: <?= h(gmdate('Y-m-d H:i')) ?> UTC · <a class="text-decoration-underline" href="xampp-setup.php">xampp-setup.php</a> · <a class="text-decoration-underline" href="healthz.php" target="_blank" rel="noopener">/healthz</a></div>
|
|
</div>
|
|
</aside>
|
|
</header>
|
|
|
|
<?php if (!$dbReady): ?>
|
|
<div class="alert alert-danger border-0 mb-4" role="alert">
|
|
<strong>Baza nije dostupna.</strong> Forma i lista su prikazane, ali spremanje neće raditi dok se MySQL veza ne podigne.
|
|
<div class="small mt-2">Za lokalni XAMPP pokreni Apache + MySQL, zatim otvori <a class="text-decoration-underline" href="xampp-setup.php">xampp-setup.php</a> da automatski kreiraš bazu <strong><?= h(DB_NAME) ?></strong>.</div>
|
|
<div class="small mt-2">Trenutna konfiguracija: <?= h(DB_USER) ?>@<?= h(DB_HOST) ?>:<?= h((string) DB_PORT) ?> / <?= h(DB_NAME) ?><?= DB_PASS === '' ? ' · bez lozinke' : ' · lozinka postavljena' ?></div>
|
|
<?php if ($dbError): ?>
|
|
<div class="small mt-2"><?= h($dbError) ?></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<main class="row g-4">
|
|
<section id="setup" class="col-12 col-xl-7">
|
|
<div class="section-card">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
|
<div>
|
|
<div class="overline mb-1">Create / input</div>
|
|
<h2 class="section-title">Dodaj POP3 račun</h2>
|
|
</div>
|
|
<span class="soft-badge">Server-side validation · encrypted password at rest</span>
|
|
</div>
|
|
|
|
<?php if (!empty($errors['form'])): ?>
|
|
<div class="alert alert-danger border-0 mb-4" role="alert"><?= h($errors['form']) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<form method="post" class="row g-3" novalidate>
|
|
<input type="hidden" name="action" value="save_account">
|
|
<div class="col-md-6">
|
|
<label for="label" class="form-label">Naziv mailboxa</label>
|
|
<input type="text" class="form-control<?= isset($errors['label']) ? ' is-invalid' : '' ?>" id="label" name="label" value="<?= h((string) $formData['label']) ?>" placeholder="npr. Lokalni support" <?= $dbReady ? '' : 'disabled' ?>>
|
|
<?php if (isset($errors['label'])): ?><div class="validation-note"><?= h($errors['label']) ?></div><?php endif; ?>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="email_address" class="form-label">Email adresa <span class="helper-text">(opcionalno)</span></label>
|
|
<input type="email" class="form-control<?= isset($errors['email_address']) ? ' is-invalid' : '' ?>" id="email_address" name="email_address" value="<?= h((string) $formData['email_address']) ?>" placeholder="mailbox@example.local" <?= $dbReady ? '' : 'disabled' ?>>
|
|
<?php if (isset($errors['email_address'])): ?><div class="validation-note"><?= h($errors['email_address']) ?></div><?php endif; ?>
|
|
</div>
|
|
<div class="col-md-8">
|
|
<label for="pop3_host" class="form-label">POP3 host</label>
|
|
<input type="text" class="form-control<?= isset($errors['pop3_host']) ? ' is-invalid' : '' ?>" id="pop3_host" name="pop3_host" value="<?= h((string) $formData['pop3_host']) ?>" placeholder="127.0.0.1" <?= $dbReady ? '' : 'disabled' ?>>
|
|
<?php if (isset($errors['pop3_host'])): ?><div class="validation-note"><?= h($errors['pop3_host']) ?></div><?php endif; ?>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="pop3_port" class="form-label">Port</label>
|
|
<input type="number" class="form-control<?= isset($errors['pop3_port']) ? ' is-invalid' : '' ?>" id="pop3_port" name="pop3_port" value="<?= h((string) $formData['pop3_port']) ?>" min="1" max="65535" <?= $dbReady ? '' : 'disabled' ?>>
|
|
<?php if (isset($errors['pop3_port'])): ?><div class="validation-note"><?= h($errors['pop3_port']) ?></div><?php endif; ?>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="security_mode" class="form-label">Sigurnost veze</label>
|
|
<select class="form-select" id="security_mode" name="security_mode" data-security-select data-port-target="#pop3_port" <?= $dbReady ? '' : 'disabled' ?>>
|
|
<option value="plain" <?= ($formData['security_mode'] ?? '') === 'plain' ? 'selected' : '' ?>>Plain (110)</option>
|
|
<option value="ssl" <?= ($formData['security_mode'] ?? '') === 'ssl' ? 'selected' : '' ?>>SSL/TLS (995)</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="sync_limit" class="form-label">Poruka za dohvat</label>
|
|
<input type="number" class="form-control<?= isset($errors['sync_limit']) ? ' is-invalid' : '' ?>" id="sync_limit" name="sync_limit" value="<?= h((string) $formData['sync_limit']) ?>" min="5" max="50" <?= $dbReady ? '' : 'disabled' ?>>
|
|
<?php if (isset($errors['sync_limit'])): ?><div class="validation-note"><?= h($errors['sync_limit']) ?></div><?php endif; ?>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="username" class="form-label">Korisničko ime</label>
|
|
<input type="text" class="form-control<?= isset($errors['username']) ? ' is-invalid' : '' ?>" id="username" name="username" value="<?= h((string) $formData['username']) ?>" placeholder="korisnik ili email" <?= $dbReady ? '' : 'disabled' ?>>
|
|
<?php if (isset($errors['username'])): ?><div class="validation-note"><?= h($errors['username']) ?></div><?php endif; ?>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label for="password" class="form-label">Lozinka</label>
|
|
<input type="password" class="form-control<?= isset($errors['password']) ? ' is-invalid' : '' ?>" id="password" name="password" value="<?= h((string) $formData['password']) ?>" placeholder="••••••••" <?= $dbReady ? '' : 'disabled' ?>>
|
|
<?php if (isset($errors['password'])): ?><div class="validation-note"><?= h($errors['password']) ?></div><?php endif; ?>
|
|
</div>
|
|
<div class="col-12">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" value="1" id="leave_on_server" name="leave_on_server" <?= !empty($formData['leave_on_server']) ? 'checked' : '' ?> <?= $dbReady ? '' : 'disabled' ?>>
|
|
<label class="form-check-label" for="leave_on_server">Ostavi poruke na serveru (read-only POP3 slice)</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 d-flex flex-wrap align-items-center gap-3 pt-2">
|
|
<button type="submit" class="btn btn-primary" <?= $dbReady ? '' : 'disabled' ?>>Spremi mailbox</button>
|
|
<span class="helper-text">Savjet za lokalni test: host <strong>127.0.0.1</strong>, port <strong>110</strong>, bez enkripcije.</span>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<aside class="col-12 col-xl-5">
|
|
<div class="section-card stack-md h-100">
|
|
<div>
|
|
<div class="overline mb-1">Confirmation / guide</div>
|
|
<h2 class="section-title">Što dobivaš u ovoj isporuci</h2>
|
|
</div>
|
|
<div class="quick-steps">
|
|
<article class="quick-step">
|
|
<h3>1. Spremanje računa</h3>
|
|
<p>POP3 postavke se validiraju na serveru i spremaju u MySQL, a lozinka se šifrira prije upisa u bazu.</p>
|
|
</article>
|
|
<article class="quick-step">
|
|
<h3>2. Live inbox ekran</h3>
|
|
<p>Nakon spremanja otvara se mailbox detalj koji čita najnovije poruke direktno preko POP3 veze.</p>
|
|
</article>
|
|
<article class="quick-step">
|
|
<h3>3. Pregled statusa</h3>
|
|
<p>Na dashboardu ostaju vidljivi status zadnje sinkronizacije, broj poruka i brzi linkovi prema svakom inboxu.</p>
|
|
</article>
|
|
</div>
|
|
<div class="surface-muted p-3 rounded-4 border">
|
|
<div class="overline mb-2">Next after MVP</div>
|
|
<ul class="help-list">
|
|
<li>SMTP compose + Sent folder tok.</li>
|
|
<li>Lokalni cache headera/poruka za bržu pretragu.</li>
|
|
<li>Uređivanje i deaktivacija mailbox računa.</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<section id="accounts" class="col-12">
|
|
<div class="section-card">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
|
<div>
|
|
<div class="overline mb-1">List</div>
|
|
<h2 class="section-title">Spremljeni mailbox računi</h2>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<span class="soft-badge">Last sync: <?= h($latestSync === 'Not yet' ? $latestSync : format_datetime($latestSync)) ?></span>
|
|
<span class="soft-badge">Accounts: <?= h((string) count($accounts)) ?></span>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($accounts): ?>
|
|
<div class="table-shell">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Mailbox</th>
|
|
<th>POP3</th>
|
|
<th>Status</th>
|
|
<th>Zadnja sinkronizacija</th>
|
|
<th class="text-end">Akcija</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($accounts as $account): ?>
|
|
<tr>
|
|
<td>
|
|
<div class="fw-semibold"><?= h($account['label']) ?></div>
|
|
<div class="small text-secondary"><?= h((string) ($account['email_address'] ?: $account['username'])) ?></div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-semibold"><?= h($account['pop3_host']) ?>:<?= h((string) $account['pop3_port']) ?></div>
|
|
<div class="small text-secondary"><?= h(security_label((string) $account['security_mode'])) ?> · limit <?= h((string) $account['sync_limit']) ?></div>
|
|
</td>
|
|
<td>
|
|
<span class="status-badge <?= h(status_tone((string) $account['last_status'])) ?>"><?= h((string) ($account['last_status'] ?: 'Ready')) ?></span>
|
|
<div class="small text-secondary mt-1"><?= h((string) $account['last_message_count']) ?> poruka</div>
|
|
</td>
|
|
<td>
|
|
<div class="fw-semibold"><?= h(format_datetime($account['last_sync_at'])) ?></div>
|
|
<div class="small text-secondary">Dodano <?= h(format_datetime($account['created_at'])) ?></div>
|
|
</td>
|
|
<td class="text-end">
|
|
<a class="btn btn-sm btn-primary" href="mailbox.php?id=<?= h((string) $account['id']) ?>">Otvori inbox</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="empty-panel">
|
|
<h3 class="h5 mb-2">Još nema spremljenih mailbox računa.</h3>
|
|
<p class="empty-copy mb-3">Dodaj prvi POP3 račun kako bi dashboard dobio live inbox i detail prikaz poruka.</p>
|
|
<div class="d-flex justify-content-center flex-wrap gap-2">
|
|
<span class="soft-badge">Primjer hosta: 127.0.0.1</span>
|
|
<span class="soft-badge">Primjer porta: 110</span>
|
|
<span class="soft-badge">Sigurnost: plain</span>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<footer class="d-flex flex-column flex-lg-row justify-content-between gap-3 pt-4 footer-copy">
|
|
<div>Thin slice is ready: create account → confirmation redirect → list → inbox detail.</div>
|
|
<div class="d-flex gap-3 flex-wrap">
|
|
<a class="text-decoration-underline" href="healthz.php" target="_blank" rel="noopener">Open /healthz</a>
|
|
<?php if ($accounts): ?>
|
|
<a class="text-decoration-underline" href="mailbox.php?id=<?= h((string) $accounts[0]['id']) ?>">Open latest inbox</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
|
|
<script src="assets/js/main.js?v=<?= h(asset_version('assets/js/main.js')) ?>" defer></script>
|
|
</body>
|
|
</html>
|