Files
fitme3070.pt/shared/platform-tools/smtp_status.php
T
andreandClaude Opus 5 4d40adbf64 Vendor platform-tools: everything this site runs is now in one repo
shared/platform-tools was a git submodule. On this branch it is a plain
directory of tracked files, frozen at ea3616abae30106df283e73a3e2f9e5bb0e6ae90
— the same commit fotografia.andmore.pt pins. Contents are byte-identical to
that tree, verified blob by blob.

The submodule caused every deployment failure this site has had:

  - .gitmodules named no `branch`, so the update fell back to
    refs/remotes/origin/HEAD — a ref a plain server-side clone never writes.
    "Atualizar submódulo" failed with "Unable to find refs/remotes/origin/HEAD
    revision in submodule path".
  - Its URL was http://, which the origin answers with 523.
  - This install is deployed by uploading files, and a submodule is invisible
    to that. The tree moved forward while shared/platform-tools stayed on the
    old pin, config/functions_secrets.php died on a missing compat_secrets.php,
    and the whole site — public pages included — went down.

Each of those was separately fixable. Together they mean a second repository
nobody can see from the deployment path, gating whether the first one boots. A
plain directory deploys like every other file and cannot fall out of step.

The cost is that platform-tools updates no longer arrive by bumping a pin; they
are a copy-over-and-commit. shared/platform-tools/VENDORED.md records the
upstream SHA, the procedure, and which four files differ from upstream in line
endings only (social/*.js, .css, functions_publications.php — CRLF upstream,
normalised to LF here; content otherwise identical).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 23:06:23 +01:00

307 lines
17 KiB
PHP

<?php
require_once __DIR__ . '/functions_settings.php'; // platform_admin_url()
// ─── Shared SMTP status view ─────────────────────────────────────────────────
//
// Bootstrap contract — the stub at <host>/views/sysadmin/smtp_status.php must:
// 1. Load $pdo 2. Run the sysadmin guard 3. Set $repo_root
// Optional overrides, same as backups.php / platform_update.php:
// $platform_layout_header / _navbar / _footer, $platform_dashboard_url
//
// Read-only by default: it reports what the transport WOULD do. The two actions
// (connection test, send a test message) are explicit and CSRF-guarded, because
// one opens a socket to a third party and the other actually sends mail.
require_once __DIR__ . '/functions_mail.php';
require_once __DIR__ . '/functions_csrf.php';
if (!isset($repo_root) || !is_string($repo_root)) {
$repo_root = realpath(dirname(__DIR__, 2)) ?: dirname(__DIR__, 2);
}
$repo_root = rtrim($repo_root, '/');
// ── Actions (POST to self, PRG) ─────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if (!platform_csrf_ok()) {
$_SESSION['smtp_flash'] = ['type' => 'danger', 'message' => 'Pedido rejeitado (token CSRF ausente ou inválido).'];
} elseif ($action === 'test_connection') {
$r = platform_mail_test($repo_root);
$_SESSION['smtp_flash'] = $r['ok']
? ['type' => 'success', 'message' => 'Ligação SMTP estabelecida e fechada com sucesso.', 'log' => $r['log']]
: ['type' => 'danger', 'message' => 'Falha na ligação: ' . $r['error'], 'log' => $r['log']];
} elseif ($action === 'save') {
// Editar aqui, e não só diagnosticar.
//
// A página nasceu só de leitura, o que fazia sentido enquanto cada site
// tivesse a sua página de Comunicações. Não têm: no hub2, no silver e no
// quack esta é a única página de SMTP que existe, e a única forma de
// mudar o servidor era o setup.php -- uma página chamada "Configuração
// inicial", que fecha assim que houver um sysadmin. Uma página que
// mostra "Servidor: — vazio —" sem dar maneira de o preencher é um beco.
//
// Os sites que TÊM Comunicações próprias redirigem daqui para lá, para
// não haver dois editores da mesma coisa.
if (!isset($pdo) || !$pdo instanceof PDO) {
// Sem base não há onde gravar. O stub deste site só forneceu
// $repo_root, por isso a página fica de leitura.
$_SESSION['smtp_flash'] = ['type' => 'danger',
'message' => 'Esta instalação não expõe a base de dados a esta página; configure pelo .env.'];
header('Location: ' . $_SERVER['REQUEST_URI'], true, 303);
exit;
}
$uid = isset($_SESSION['user']['id']) ? (int) $_SESSION['user']['id'] : null;
$port = (int) ($_POST['smtp_port'] ?? 587);
$secure = strtolower(trim((string) ($_POST['smtp_secure'] ?? 'tls')));
if (!in_array($secure, ['tls', 'ssl', ''], true)) {
$secure = 'tls';
}
$from = trim((string) ($_POST['email_from'] ?? ''));
if ($from !== '' && !filter_var($from, FILTER_VALIDATE_EMAIL)) {
// Validado porque já se perdeu tempo com isto: o campo do remetente
// ficou uma vez com o NOME do remetente lá dentro, e o erro só
// apareceu como "Invalid address (From)" numa fila presa, horas
// depois. Um formulário que aceita tudo empurra a falha para longe
// de quem a pode corrigir.
$_SESSION['smtp_flash'] = ['type' => 'danger',
'message' => 'O remetente tem de ser um endereço de email — o nome vai no campo abaixo.'];
header('Location: ' . $_SERVER['REQUEST_URI'], true, 303);
exit;
}
platform_settings_set($pdo, 'smtp_host', trim((string) ($_POST['smtp_host'] ?? '')), $uid);
platform_settings_set($pdo, 'smtp_port', (string) ($port > 0 ? $port : 587), $uid);
platform_settings_set($pdo, 'smtp_secure', $secure, $uid);
platform_settings_set($pdo, 'smtp_username', trim((string) ($_POST['smtp_username'] ?? '')), $uid);
platform_settings_set($pdo, 'email_from', $from, $uid);
platform_settings_set($pdo, 'email_from_name', trim((string) ($_POST['email_from_name'] ?? '')), $uid);
// A password nunca é pré-preenchida, por isso vazio significa "mantém a
// que está" e não "apaga". Apagar é uma caixa explícita.
if (!empty($_POST['smtp_password_clear'])) {
platform_settings_set($pdo, 'smtp_password', '', $uid);
} elseif (trim((string) ($_POST['smtp_password'] ?? '')) !== '') {
platform_settings_set($pdo, 'smtp_password', trim((string) $_POST['smtp_password']), $uid);
}
$_SESSION['smtp_flash'] = ['type' => 'success', 'message' => 'Configuração guardada.'];
} elseif ($action === 'send_test') {
$to = trim((string) ($_POST['to'] ?? ''));
if (!filter_var($to, FILTER_VALIDATE_EMAIL)) {
$_SESSION['smtp_flash'] = ['type' => 'danger', 'message' => 'Endereço de destino inválido.'];
} else {
$r = platform_mail_send([
'to' => $to,
'to_name' => 'Teste',
'subject' => 'Teste de SMTP — ' . (string) ($_SERVER['HTTP_HOST'] ?? 'plataforma'),
'body' => '<p>Mensagem de teste enviada a partir do painel de sistema.</p>',
'alt_body' => 'Mensagem de teste enviada a partir do painel de sistema.',
], $repo_root);
if (!empty($r['skipped'])) {
$_SESSION['smtp_flash'] = ['type' => 'warning',
'message' => 'Não enviado: EMAIL_DEV_PRINT está activo e APP_ENV não é prod, por isso a mensagem foi apenas registada no log.'];
} elseif (!empty($r['ok'])) {
$_SESSION['smtp_flash'] = ['type' => 'success', 'message' => 'Mensagem entregue ao servidor SMTP para ' . $to . '.'];
} else {
$_SESSION['smtp_flash'] = ['type' => 'danger', 'message' => 'Falha no envio: ' . $r['error']];
}
}
}
header('Location: ' . $_SERVER['REQUEST_URI'], true, 303);
exit;
}
// Editar exige base de dados: é lá que as definições ficam. Um stub que só
// forneça $repo_root continua a ver a página, em modo de leitura.
$canEdit = isset($pdo) && $pdo instanceof PDO;
$flash = $_SESSION['smtp_flash'] ?? null;
unset($_SESSION['smtp_flash']);
// A configuração que realmente envia, e não a do ambiente: com $pdo, é a da
// base que ganha. Era daqui que vinha o formulário mostrar campos vazios num
// site cujo email funcionava.
$cfg = (isset($pdo) && $pdo instanceof PDO && function_exists('platform_smtp_config'))
? platform_mail_config(platform_smtp_config($pdo))
: platform_mail_config();
$hasMailer = platform_mail_available($repo_root);
$isProd = ($cfg['env'] === 'prod');
// Veredicto e valores em vigor: uma implementação só, em functions_mail.php.
// Estavam aqui em linha, e quando o comms.php passou a mostrar o mesmo passaram
// a ser duas cópias da mesma lista de casos -- a que ficasse para trás mentiria
// a alguém a meio de uma avaria.
$verdict = platform_mail_diagnosis($repo_root, null, $pdo ?? null);
$rows = platform_mail_config_rows(null, $pdo ?? null);
$layout_header = $platform_layout_header ?? ($repo_root . '/includes/header.php');
$layout_navbar = $platform_layout_navbar ?? ($repo_root . '/includes/navbar.php');
$layout_footer = $platform_layout_footer ?? ($repo_root . '/includes/footer.php');
$dashboard_url = $platform_dashboard_url
?? (function_exists('base_url') ? platform_admin_url('index.php') : 'index.php');
$pageTitle = 'Estado SMTP';
if (is_file($layout_header)) include $layout_header;
if (is_file($layout_navbar)) include $layout_navbar;
?>
<div class="container py-4" style="max-width:900px;">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
<div>
<h2 class="mb-1"><i class="bi bi-envelope-check me-2"></i>Estado SMTP</h2>
<p class="text-muted small mb-0">Configuração do transporte de email partilhado e testes de ligação.</p>
</div>
<a href="<?= htmlspecialchars($dashboard_url) ?>" class="btn btn-outline-secondary btn-sm">Voltar</a>
</div>
<?php if (is_array($flash)): ?>
<div class="alert alert-<?= htmlspecialchars($flash['type']) ?> alert-dismissible fade show" role="alert">
<?= htmlspecialchars((string) $flash['message']) ?>
<?php if (!empty($flash['log'])): ?>
<pre class="small mt-2 mb-0" style="max-height:220px;overflow:auto;white-space:pre-wrap;"><?= htmlspecialchars((string) $flash['log']) ?></pre>
<?php endif; ?>
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
<?php endif; ?>
<div class="card shadow-sm mb-3 border-<?= htmlspecialchars($verdict[0]) ?>">
<div class="card-body d-flex align-items-start gap-3">
<span class="badge text-bg-<?= htmlspecialchars($verdict[0]) ?> mt-1"><?= htmlspecialchars($verdict[1]) ?></span>
<div class="small mb-0"><?= htmlspecialchars($verdict[2]) ?></div>
</div>
</div>
<div class="card shadow-sm mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<strong><i class="bi bi-sliders me-1"></i>Configuração</strong>
<span class="badge text-bg-<?= $hasMailer ? 'success' : 'danger' ?>">
PHPMailer <?= $hasMailer ? 'disponível' : 'em falta' ?>
</span>
</div>
<div class="table-responsive">
<table class="table table-sm mb-0 small align-middle">
<tbody>
<?php foreach ($rows as [$k, $v, $ok]): ?>
<tr>
<td class="font-monospace text-muted" style="width:220px;"><?= htmlspecialchars($k) ?></td>
<td class="font-monospace text-break"><?= htmlspecialchars($v) ?></td>
<td class="text-end" style="width:40px;">
<i class="bi bi-<?= $ok ? 'check-circle text-success' : 'exclamation-circle text-warning' ?>"></i>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="card-footer small text-muted">
Valores lidos por ordem: constante definida pela aplicação &rarr; <code>.env</code>.
Onde a aplicação guarda definições em base de dados, essas ganham sobre ambas.
</div>
</div>
<?php if ($canEdit): ?>
<div class="card shadow-sm mb-3">
<div class="card-header"><strong><i class="bi bi-pencil-square me-1"></i>Alterar</strong></div>
<div class="card-body">
<form method="post" class="row g-3">
<?= platform_csrf_field() ?>
<input type="hidden" name="action" value="save">
<div class="col-md-8">
<label class="form-label small">Servidor SMTP</label>
<input type="text" name="smtp_host" class="form-control form-control-sm"
value="<?= htmlspecialchars($cfg['host']) ?>" placeholder="mail.exemplo.pt">
<div class="form-text">No cPanel é tipicamente <code>mail.&lt;dominio&gt;</code>.</div>
</div>
<div class="col-md-2">
<label class="form-label small">Porta</label>
<input type="number" name="smtp_port" class="form-control form-control-sm"
value="<?= (int) $cfg['port'] ?>">
</div>
<div class="col-md-2">
<label class="form-label small">Cifra</label>
<select name="smtp_secure" class="form-select form-select-sm">
<?php foreach (['tls' => 'TLS', 'ssl' => 'SSL', '' => 'Nenhuma'] as $v => $lbl): ?>
<option value="<?= $v ?>" <?= $cfg['secure'] === $v ? 'selected' : '' ?>><?= $lbl ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-6">
<label class="form-label small">Utilizador</label>
<input type="text" name="smtp_username" class="form-control form-control-sm"
value="<?= htmlspecialchars($cfg['username']) ?>" autocomplete="off">
<div class="form-text">Normalmente o endereço de email completo.</div>
</div>
<div class="col-md-6">
<label class="form-label small">Palavra-passe</label>
<input type="password" name="smtp_password" class="form-control form-control-sm"
placeholder="<?= $cfg['password'] !== '' ? '•••••••• (definida — deixe vazio para manter)' : 'por definir' ?>"
autocomplete="new-password">
<?php if ($cfg['password'] !== ''): ?>
<div class="form-check mt-1">
<input class="form-check-input" type="checkbox" name="smtp_password_clear" id="pwClear" value="1">
<label class="form-check-label small text-muted" for="pwClear">Apagar a palavra-passe guardada</label>
</div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label small">Remetente (From)</label>
<input type="email" name="email_from" class="form-control form-control-sm"
value="<?= htmlspecialchars($cfg['from']) ?>" placeholder="noreply@exemplo.pt">
<div class="form-text">Tem de ser a conta autenticada acima, senão SPF/DMARC rejeitam.</div>
</div>
<div class="col-md-6">
<label class="form-label small">Nome do remetente</label>
<input type="text" name="email_from_name" class="form-control form-control-sm"
value="<?= htmlspecialchars($cfg['from_name']) ?>" placeholder="A Minha Plataforma">
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-check-lg me-1"></i>Guardar
</button>
<span class="small text-muted ms-2">
Guardado em base de dados, que ganha ao <code>.env</code>. Teste a ligação a seguir.
</span>
</div>
</form>
</div>
</div>
<?php endif; ?>
<div class="card shadow-sm">
<div class="card-header"><strong><i class="bi bi-activity me-1"></i>Testes</strong></div>
<div class="card-body d-flex flex-wrap gap-3 align-items-end">
<form method="POST" class="m-0">
<?= platform_csrf_field() ?><input type="hidden" name="action" value="test_connection">
<button type="submit" class="btn btn-outline-primary btn-sm" <?= $cfg['host'] === '' ? 'disabled' : '' ?>>
<i class="bi bi-plug me-1"></i>Testar ligação
</button>
<div class="form-text">Abre e fecha uma ligação. Não envia nada.</div>
</form>
<form method="POST" class="m-0 d-flex gap-2 align-items-end flex-wrap">
<?= platform_csrf_field() ?><input type="hidden" name="action" value="send_test">
<div>
<label class="form-label small mb-1" for="smtp_to">Enviar mensagem de teste para</label>
<input type="email" name="to" id="smtp_to" class="form-control form-control-sm"
style="min-width:260px;" placeholder="alguem@exemplo.pt" required>
</div>
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-send me-1"></i>Enviar
</button>
</form>
</div>
<div class="card-footer small text-muted">
<i class="bi bi-info-circle me-1"></i>
Um teste bem sucedido significa que o servidor SMTP <em>aceitou</em> a mensagem —
não que ela chegou à caixa de entrada. Rejeições por SPF, DKIM ou reputação acontecem depois.
</div>
</div>
</div>
<?php if (is_file($layout_footer)) include $layout_footer; ?>