mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
smtp: probe credentials on save (connect+auth+quit, no message sent)
This commit is contained in:
@@ -294,6 +294,135 @@ class SmtpRelay {
|
||||
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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user