mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
smtp: probe credentials on save (connect+auth+quit, no message sent)
This commit is contained in:
6
TODO.md
6
TODO.md
@@ -63,6 +63,12 @@
|
|||||||
- [ ] Verify TCP reachability from XAMXAM VM to LDAP server (port 636)
|
- [ ] Verify TCP reachability from XAMXAM VM to LDAP server (port 636)
|
||||||
- [ ] See `docs/LDAP_AUTH_PLAN.md` for full phase-by-phase plan
|
- [ ] See `docs/LDAP_AUTH_PLAN.md` for full phase-by-phase plan
|
||||||
|
|
||||||
|
## SMTP credential validation
|
||||||
|
|
||||||
|
- [x] Add `SmtpRelay::test()` — connect + EHLO + STARTTLS + AUTH + QUIT, no message sent
|
||||||
|
- [x] Add `SmtpRelay::smtpProbe()` — private low-level probe (mirrors smtpSend without envelope/data)
|
||||||
|
- [x] Wire into `actions/settings.php` SMTP branch: probe immediately after save, flash success or error with detail
|
||||||
|
|
||||||
## Répertoire layout
|
## Répertoire layout
|
||||||
|
|
||||||
- [x] Make column headings sticky/non-scrollable; only `ul` scrolls per column
|
- [x] Make column headings sticky/non-scrollable; only `ul` scrolls per column
|
||||||
|
|||||||
@@ -47,7 +47,14 @@ if ($section === 'formulaire') {
|
|||||||
$smtpData['password'] = $pwd;
|
$smtpData['password'] = $pwd;
|
||||||
}
|
}
|
||||||
SmtpRelay::updateSettings($db, $smtpData);
|
SmtpRelay::updateSettings($db, $smtpData);
|
||||||
App::flash('success', "Paramètres SMTP mis à jour.");
|
|
||||||
|
// Immediately probe the server to validate credentials
|
||||||
|
$test = SmtpRelay::test($db);
|
||||||
|
if ($test['ok']) {
|
||||||
|
App::flash('success', "Paramètres SMTP mis à jour — connexion validée avec succès ✓");
|
||||||
|
} else {
|
||||||
|
App::flash('error', "Paramètres sauvegardés, mais le test de connexion SMTP a échoué : " . $test['error']);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
App::flash('error', "Section inconnue.");
|
App::flash('error', "Section inconnue.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,6 +294,135 @@ class SmtpRelay {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test the stored SMTP credentials without sending any message.
|
||||||
|
*
|
||||||
|
* Opens a socket, performs EHLO, upgrades to TLS if configured, authenticates,
|
||||||
|
* then immediately sends QUIT. No MAIL FROM / RCPT TO / DATA is issued,
|
||||||
|
* so nothing lands in any mailbox.
|
||||||
|
*
|
||||||
|
* @return array{ok:bool, error:string}
|
||||||
|
*/
|
||||||
|
public static function test(Database $db): array
|
||||||
|
{
|
||||||
|
$s = self::getSettings($db);
|
||||||
|
if ($s['host'] === '') {
|
||||||
|
return ['ok' => false, 'error' => 'Hôte SMTP non configuré.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
self::smtpProbe($s);
|
||||||
|
return ['ok' => true, 'error' => ''];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ['ok' => false, 'error' => $e->getMessage()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like smtpSend() but stops after AUTH — no envelope, no message.
|
||||||
|
*/
|
||||||
|
private static function smtpProbe(array $s): void
|
||||||
|
{
|
||||||
|
$host = $s['host'];
|
||||||
|
$port = (int) $s['port'];
|
||||||
|
$encryption = $s['encryption'];
|
||||||
|
$timeout = 10;
|
||||||
|
|
||||||
|
$connectHost = ($encryption === 'ssl') ? "ssl://{$host}" : $host;
|
||||||
|
|
||||||
|
$errno = 0; $errstr = '';
|
||||||
|
$ctx = stream_context_create([
|
||||||
|
'ssl' => [
|
||||||
|
'verify_peer' => false,
|
||||||
|
'verify_peer_name' => false,
|
||||||
|
'allow_self_signed' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$sock = @stream_socket_client(
|
||||||
|
"{$connectHost}:{$port}", $errno, $errstr, $timeout,
|
||||||
|
STREAM_CLIENT_CONNECT, $ctx
|
||||||
|
);
|
||||||
|
if ($sock === false) {
|
||||||
|
throw new \RuntimeException("Connexion impossible à {$host}:{$port} — {$errstr} [{$errno}]");
|
||||||
|
}
|
||||||
|
stream_set_timeout($sock, $timeout);
|
||||||
|
|
||||||
|
$read = function () use ($sock, $timeout): string {
|
||||||
|
$buf = '';
|
||||||
|
while (($line = fgets($sock, 512)) !== false) {
|
||||||
|
$buf .= $line;
|
||||||
|
if (isset($line[3]) && $line[3] === ' ') break;
|
||||||
|
$meta = stream_get_meta_data($sock);
|
||||||
|
if ($meta['timed_out']) break;
|
||||||
|
}
|
||||||
|
return $buf;
|
||||||
|
};
|
||||||
|
$send = function (string $cmd) use ($sock, $read): string {
|
||||||
|
fwrite($sock, $cmd . "\r\n");
|
||||||
|
return $read();
|
||||||
|
};
|
||||||
|
$expect = function (string $cmd, string $code) use ($send): string {
|
||||||
|
$resp = $send($cmd);
|
||||||
|
if (strncmp($resp, $code, strlen($code)) !== 0) {
|
||||||
|
throw new \RuntimeException("Réponse SMTP inattendue pour '{$cmd}' : " . trim($resp));
|
||||||
|
}
|
||||||
|
return $resp;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Greeting
|
||||||
|
$greeting = $read();
|
||||||
|
if (strncmp($greeting, '220', 3) !== 0) {
|
||||||
|
$meta = stream_get_meta_data($sock);
|
||||||
|
$detail = $meta['timed_out']
|
||||||
|
? '(délai dépassé — aucune donnée reçue)'
|
||||||
|
: '(reçu : ' . json_encode(trim($greeting)) . ')';
|
||||||
|
throw new \RuntimeException("Salutation SMTP invalide {$detail}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$parseEhlo = function (string $resp): array {
|
||||||
|
$caps = [];
|
||||||
|
foreach (explode("\n", $resp) as $line) {
|
||||||
|
$line = rtrim($line);
|
||||||
|
if (strlen($line) > 4) $caps[] = strtoupper(substr($line, 4));
|
||||||
|
}
|
||||||
|
return $caps;
|
||||||
|
};
|
||||||
|
|
||||||
|
$ehloResp = $send('EHLO ' . gethostname());
|
||||||
|
$caps = strncmp($ehloResp, '250', 3) === 0 ? $parseEhlo($ehloResp) : [];
|
||||||
|
if (empty($caps)) {
|
||||||
|
$send('HELO ' . gethostname());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($encryption === 'tls') {
|
||||||
|
$expect('STARTTLS', '220');
|
||||||
|
if (!stream_socket_enable_crypto($sock, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
||||||
|
throw new \RuntimeException('Échec de la négociation TLS (vérifiez le certificat du serveur)');
|
||||||
|
}
|
||||||
|
$ehloResp = $send('EHLO ' . gethostname());
|
||||||
|
$caps = $parseEhlo($ehloResp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($s['username'] !== '') {
|
||||||
|
$authLine = '';
|
||||||
|
foreach ($caps as $cap) {
|
||||||
|
if (strncmp($cap, 'AUTH', 4) === 0) { $authLine = $cap; break; }
|
||||||
|
}
|
||||||
|
$mechanisms = preg_split('/[\s=]+/', $authLine);
|
||||||
|
if (in_array('PLAIN', $mechanisms, true)) {
|
||||||
|
$token = base64_encode("\0{$s['username']}\0{$s['password']}");
|
||||||
|
$expect("AUTH PLAIN {$token}", '235');
|
||||||
|
} else {
|
||||||
|
$expect('AUTH LOGIN', '334');
|
||||||
|
$expect(base64_encode($s['username']), '334');
|
||||||
|
$expect(base64_encode($s['password']), '235');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$send('QUIT');
|
||||||
|
fclose($sock);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queue (persist) an e-mail for deferred sending.
|
* Queue (persist) an e-mail for deferred sending.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
{"source":"partage","action":"submit","status":"success","thesis_id":15,"identifier":"2025-012","author":"Emma Renard","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T09:20:16+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
{"source":"partage","action":"submit","status":"success","thesis_id":15,"identifier":"2025-012","author":"Emma Renard","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T09:20:16+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
||||||
{"source":"partage","action":"submit","status":"success","thesis_id":16,"identifier":"2025-013","author":"Emma Renard","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T09:35:49+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
{"source":"partage","action":"submit","status":"success","thesis_id":16,"identifier":"2025-013","author":"Emma Renard","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T09:35:49+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
||||||
|
{"source":"partage","action":"submit","status":"success","thesis_id":17,"identifier":"2025-014","author":"Théo Marchand","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T09:48:20+00:00","ip":"127.0.0.1","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"}
|
||||||
|
|||||||
Reference in New Issue
Block a user