Lisää SMTP-testaustyökalu postilaatikon asetuksiin

Korvaa mailbox_debug-endpoint kattavalla smtp_test-endpointilla,
joka testaa yhteyden, TLS:n ja autentikoinnin vaihe vaiheelta.
"Testaa SMTP" -nappi lomakkeessa näyttää tulokset.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:57:34 +02:00
parent d5b015d71a
commit 1a41579e4b
3 changed files with 178 additions and 18 deletions

157
api.php
View File

@@ -2975,26 +2975,149 @@ switch ($action) {
echo json_encode($mbs); echo json_encode($mbs);
break; break;
case 'mailbox_debug': case 'smtp_test':
requireAdmin(); requireAdmin();
$companyId = requireCompany(); $companyId = requireCompany();
$mailboxes = dbLoadMailboxes($companyId); $input = json_decode(file_get_contents('php://input'), true);
$debug = array_map(function($mb) { $mailboxId = $input['mailbox_id'] ?? '';
return [ $mailbox = dbGetMailbox($mailboxId);
'id' => $mb['id'], if (!$mailbox) {
'nimi' => $mb['nimi'], echo json_encode(['error' => 'Postilaatikkoa ei löydy']);
'imap_host' => $mb['imap_host'] ?? '', break;
'imap_user' => $mb['imap_user'] ?? '', }
'imap_pass_len' => strlen($mb['imap_password'] ?? ''), // Näytä tietokannan raakatiedot
'smtp_host' => $mb['smtp_host'] ?? '', $dbInfo = [
'smtp_port' => $mb['smtp_port'] ?? '', 'imap_user' => $mailbox['imap_user'] ?? '',
'smtp_user' => $mb['smtp_user'] ?? '', 'imap_pass_len' => strlen($mailbox['imap_password'] ?? ''),
'smtp_pass_len' => strlen($mb['smtp_password'] ?? ''), 'smtp_host' => $mailbox['smtp_host'] ?? '',
'smtp_encryption' => $mb['smtp_encryption'] ?? '', 'smtp_port' => $mailbox['smtp_port'] ?? '',
'smtp_from_email' => $mb['smtp_from_email'] ?? '', 'smtp_user' => $mailbox['smtp_user'] ?? '',
'smtp_pass_len' => strlen($mailbox['smtp_password'] ?? ''),
'smtp_encryption' => $mailbox['smtp_encryption'] ?? '',
'smtp_from_email' => $mailbox['smtp_from_email'] ?? '',
]; ];
}, $mailboxes); // Laske fallback-arvot
echo json_encode($debug, JSON_PRETTY_PRINT); $effectiveUser = $mailbox['smtp_user'] ?? '';
if ($effectiveUser === '') $effectiveUser = $mailbox['imap_user'] ?? '';
if ($effectiveUser === '') $effectiveUser = $mailbox['smtp_from_email'] ?? '';
$effectivePass = $mailbox['smtp_password'] ?? '';
if ($effectivePass === '') $effectivePass = $mailbox['imap_password'] ?? '';
$host = $mailbox['smtp_host'] ?? '';
$port = (int)($mailbox['smtp_port'] ?? 587);
$encryption = $mailbox['smtp_encryption'] ?? 'tls';
$result = [
'db_values' => $dbInfo,
'effective_user' => $effectiveUser,
'effective_pass_len' => strlen($effectivePass),
'steps' => [],
];
if (empty($host)) {
$result['steps'][] = '❌ SMTP-palvelin puuttuu';
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}
if (empty($effectivePass)) {
$result['steps'][] = '❌ Salasana puuttuu (tietokannassa ei SMTP- eikä IMAP-salasanaa)';
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}
// Testaa yhteys
$connStr = ($encryption === 'ssl' ? 'ssl' : 'tcp') . "://{$host}:{$port}";
$result['steps'][] = "🔌 Yhdistetään: {$connStr}";
$ctx = stream_context_create(['ssl' => ['verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true]]);
$fp = @stream_socket_client($connStr, $errno, $errstr, 15, STREAM_CLIENT_CONNECT, $ctx);
if (!$fp) {
$result['steps'][] = "❌ Yhteys epäonnistui: {$errstr} ({$errno})";
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}
stream_set_timeout($fp, 15);
$result['steps'][] = '✅ Yhteys muodostettu';
// Banner
$resp = smtpReadResponse($fp);
$result['steps'][] = '📨 Banner: ' . trim($resp);
if (smtpCode($resp) !== '220') {
$result['steps'][] = '❌ Virheellinen banner';
fclose($fp);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}
// EHLO
$ehlo = smtpCommand($fp, "EHLO " . gethostname());
$result['steps'][] = '📨 EHLO: ' . smtpCode($ehlo);
$result['ehlo_capabilities'] = trim($ehlo);
if (smtpCode($ehlo) !== '250') {
$result['steps'][] = '❌ EHLO hylätty';
fclose($fp);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}
// STARTTLS
if ($encryption === 'tls') {
$resp = smtpCommand($fp, "STARTTLS");
$result['steps'][] = '🔒 STARTTLS: ' . smtpCode($resp);
if (smtpCode($resp) !== '220') {
$result['steps'][] = '❌ STARTTLS hylätty: ' . trim($resp);
fclose($fp);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}
$crypto = @stream_socket_enable_crypto($fp, true,
STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT);
if (!$crypto) {
$result['steps'][] = '❌ TLS-neuvottelu epäonnistui';
fclose($fp);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}
$result['steps'][] = '✅ TLS OK';
// Re-EHLO
$ehlo = smtpCommand($fp, "EHLO " . gethostname());
$result['steps'][] = '📨 EHLO2: ' . smtpCode($ehlo);
$result['ehlo_capabilities_tls'] = trim($ehlo);
}
// AUTH PLAIN
$cred = base64_encode("\0{$effectiveUser}\0{$effectivePass}");
$resp = smtpCommand($fp, "AUTH PLAIN {$cred}");
$result['steps'][] = '🔑 AUTH PLAIN: ' . trim($resp);
if (smtpCode($resp) === '235') {
$result['steps'][] = '✅ Kirjautuminen onnistui (AUTH PLAIN)!';
smtpCommand($fp, "QUIT");
fclose($fp);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}
// AUTH LOGIN fallback
$resp = smtpCommand($fp, "AUTH LOGIN");
$result['steps'][] = '🔑 AUTH LOGIN: ' . smtpCode($resp);
if (smtpCode($resp) === '334') {
$resp = smtpCommand($fp, base64_encode($effectiveUser));
$result['steps'][] = '🔑 Käyttäjä: ' . smtpCode($resp);
if (smtpCode($resp) === '334') {
$resp = smtpCommand($fp, base64_encode($effectivePass));
$result['steps'][] = '🔑 Salasana: ' . trim($resp);
if (smtpCode($resp) === '235') {
$result['steps'][] = '✅ Kirjautuminen onnistui (AUTH LOGIN)!';
smtpCommand($fp, "QUIT");
fclose($fp);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break;
}
}
}
$result['steps'][] = "❌ Kirjautuminen epäonnistui (käyttäjä: {$effectiveUser}, salasanan pituus: " . strlen($effectivePass) . ")";
smtpCommand($fp, "QUIT");
fclose($fp);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
break; break;
case 'mailbox_save': case 'mailbox_save':

View File

@@ -859,8 +859,10 @@
</div> </div>
<div style="display:flex;gap:0.5rem;margin-top:0.75rem;"> <div style="display:flex;gap:0.5rem;margin-top:0.75rem;">
<button class="btn-primary" id="btn-save-mailbox">Tallenna</button> <button class="btn-primary" id="btn-save-mailbox">Tallenna</button>
<button class="btn-secondary" id="btn-test-smtp" style="background:#2196F3;color:#fff;border:none;">🔌 Testaa SMTP</button>
<button class="btn-secondary" id="btn-cancel-mailbox">Peruuta</button> <button class="btn-secondary" id="btn-cancel-mailbox">Peruuta</button>
</div> </div>
<pre id="smtp-test-result" style="display:none;background:#1a1a2e;color:#0f0;padding:0.75rem;border-radius:6px;font-size:0.8rem;max-height:300px;overflow:auto;margin-top:0.5rem;white-space:pre-wrap;"></pre>
</div> </div>
</div> </div>
<!-- Sijainnit (Sites) --> <!-- Sijainnit (Sites) -->

View File

@@ -2600,6 +2600,41 @@ document.getElementById('btn-cancel-mailbox').addEventListener('click', () => {
document.getElementById('mailbox-form-container').style.display = 'none'; document.getElementById('mailbox-form-container').style.display = 'none';
}); });
// SMTP-testaus
document.getElementById('btn-test-smtp').addEventListener('click', async () => {
const mailboxId = document.getElementById('mailbox-form-id').value;
const resultEl = document.getElementById('smtp-test-result');
if (!mailboxId) {
resultEl.style.display = '';
resultEl.textContent = '⚠️ Tallenna postilaatikko ensin, sitten testaa.';
return;
}
resultEl.style.display = '';
resultEl.textContent = '⏳ Testataan SMTP-yhteyttä...';
try {
const res = await apiCall('smtp_test', 'POST', { mailbox_id: mailboxId });
let output = '=== TIETOKANNAN ARVOT ===\n';
if (res.db_values) {
for (const [k, v] of Object.entries(res.db_values)) {
output += ` ${k}: ${v === '' ? '(tyhjä)' : v}\n`;
}
}
output += `\n=== KÄYTETTÄVÄT ARVOT ===\n`;
output += ` Käyttäjä: ${res.effective_user || '(tyhjä)'}\n`;
output += ` Salasanan pituus: ${res.effective_pass_len}\n\n`;
output += `=== TESTIN VAIHEET ===\n`;
if (res.steps) {
res.steps.forEach(s => { output += ` ${s}\n`; });
}
if (res.ehlo_capabilities_tls) {
output += `\n=== EHLO (TLS jälkeen) ===\n${res.ehlo_capabilities_tls}\n`;
}
resultEl.textContent = output;
} catch (e) {
resultEl.textContent = '❌ Virhe: ' + e.message;
}
});
// ==================== YRITYKSEN KÄYTTÄJÄOIKEUDET ==================== // ==================== YRITYKSEN KÄYTTÄJÄOIKEUDET ====================
async function loadCompanyUsers(companyId) { async function loadCompanyUsers(companyId) {