mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-06 19:19:19 +02:00
Handle SMTP 550 recipient-rejected errors with structured SmtpSendException
- Add SmtpSendException with smtpCode/smtpResponse/isRecipientRejected() - smtpSend() $expect closure throws SmtpSendException (with code) instead of RuntimeException - SmtpRelay::send() re-throws SmtpSendException so callers can inspect it - request-access.php (new): catch 550 → roll back token+approval, return HTTP 422 with FR user message - request-access.php (resend): catch 550 → HTTP 422 instead of silently claiming success - StudentEmail::sendConfirmation(): catch SmtpSendException → log+false (submission not aborted) - admin/actions/access-request.php: catch SmtpSendException post-approval → flash warning (recipient-rejected vs transient)
This commit is contained in:
10
TODO.md
10
TODO.md
@@ -106,6 +106,16 @@
|
|||||||
- [x] Minimal horizontal padding inside columns (`var(--space-2xs)`)
|
- [x] Minimal horizontal padding inside columns (`var(--space-2xs)`)
|
||||||
- [x] Align all column headings to the same baseline row (2-row grid via `display: contents`)
|
- [x] Align all column headings to the same baseline row (2-row grid via `display: contents`)
|
||||||
|
|
||||||
|
## SMTP 550 recipient-rejected handling
|
||||||
|
|
||||||
|
- [x] Add `SmtpSendException` — carries `smtpCode` + `smtpResponse`; `isRecipientRejected()` for 550–554
|
||||||
|
- [x] `smtpSend()` `$expect` closure throws `SmtpSendException` (with code) instead of plain `RuntimeException`
|
||||||
|
- [x] `SmtpRelay::send()` re-throws `SmtpSendException` so callers can react
|
||||||
|
- [x] `request-access.php` (new auto-approve): catch 550 → roll back token + approval, return HTTP 422 with user-facing message
|
||||||
|
- [x] `request-access.php` (resend path): catch 550 → return HTTP 422 instead of silent "access approved"
|
||||||
|
- [x] `StudentEmail::sendConfirmation()`: catch `SmtpSendException` → log + return false (submission must not be aborted)
|
||||||
|
- [x] `admin/actions/access-request.php`: catch `SmtpSendException` after approval → flash warning distinguishing recipient-rejected vs transient
|
||||||
|
|
||||||
## CSS refactor
|
## CSS refactor
|
||||||
|
|
||||||
- [x] Move semantic HTML element baseline styles into common.css
|
- [x] Move semantic HTML element baseline styles into common.css
|
||||||
|
|||||||
@@ -52,9 +52,16 @@ try {
|
|||||||
$body = buildApprovalEmail($thesisTitle, $thesisAuthors, $accessUrl, $notes);
|
$body = buildApprovalEmail($thesisTitle, $thesisAuthors, $accessUrl, $notes);
|
||||||
$plain = strip_tags($body);
|
$plain = strip_tags($body);
|
||||||
|
|
||||||
SmtpRelay::send($db, $request['email'], $subject, $body, $plain);
|
try {
|
||||||
|
SmtpRelay::send($db, $request['email'], $subject, $body, $plain);
|
||||||
App::flash('success', "Demande approuvée. Email envoyé à {$request['email']}.");
|
App::flash('success', "Demande approuvée. Email envoyé à {$request['email']}.");
|
||||||
|
} catch (SmtpSendException $e) {
|
||||||
|
error_log('[access-request] Email delivery failed after approval: ' . $e->getMessage());
|
||||||
|
$smtpMsg = $e->isRecipientRejected()
|
||||||
|
? "Demande approuvée, mais l'email n'a pas pu être délivré : adresse inconnue ({$request['email']})."
|
||||||
|
: "Demande approuvée, mais l'envoi de l'email a échoué (erreur SMTP). L'utilisateur devra relancer une demande.";
|
||||||
|
App::flash('warning', $smtpMsg);
|
||||||
|
}
|
||||||
|
|
||||||
} elseif ($action === 'reject') {
|
} elseif ($action === 'reject') {
|
||||||
$db->rejectAccessRequest($requestId, $notes);
|
$db->rejectAccessRequest($requestId, $notes);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ require_once __DIR__ . '/../bootstrap.php';
|
|||||||
require_once APP_ROOT . '/src/Database.php';
|
require_once APP_ROOT . '/src/Database.php';
|
||||||
require_once APP_ROOT . '/src/RateLimit.php';
|
require_once APP_ROOT . '/src/RateLimit.php';
|
||||||
require_once APP_ROOT . '/src/SmtpRelay.php';
|
require_once APP_ROOT . '/src/SmtpRelay.php';
|
||||||
|
// SmtpSendException is defined in SmtpRelay.php
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
@@ -114,6 +115,18 @@ if ($existingRequest) {
|
|||||||
'message' => 'Un nouvel email d\'accès vous a été envoyé.',
|
'message' => 'Un nouvel email d\'accès vous a été envoyé.',
|
||||||
'status' => 'resent',
|
'status' => 'resent',
|
||||||
]);
|
]);
|
||||||
|
} catch (SmtpSendException $e) {
|
||||||
|
error_log('Access request resend failed: ' . $e->getMessage());
|
||||||
|
if ($e->isRecipientRejected()) {
|
||||||
|
http_response_code(422);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => "L'adresse e-mail « {$email} » est introuvable sur le serveur de messagerie de l'ERG. Vérifiez l'orthographe ou utilisez une autre adresse.",
|
||||||
|
'status' => 'recipient_rejected',
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Votre accès est déjà approuvé. Si vous n\'avez pas reçu l\'email, contactez l\'administrateur.']);
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('Access request resend failed: ' . $e->getMessage());
|
error_log('Access request resend failed: ' . $e->getMessage());
|
||||||
echo json_encode(['success' => true, 'message' => 'Votre accès est déjà approuvé. Si vous n\'avez pas reçu l\'email, contactez l\'administrateur.']);
|
echo json_encode(['success' => true, 'message' => 'Votre accès est déjà approuvé. Si vous n\'avez pas reçu l\'email, contactez l\'administrateur.']);
|
||||||
@@ -152,7 +165,31 @@ try {
|
|||||||
$body = buildAutoApprovalEmail($thesis['title'], $thesis['authors'] ?? '', $accessUrl);
|
$body = buildAutoApprovalEmail($thesis['title'], $thesis['authors'] ?? '', $accessUrl);
|
||||||
$plain = htmlToPlain($body);
|
$plain = htmlToPlain($body);
|
||||||
|
|
||||||
SmtpRelay::send($db, $email, $subject, $body, $plain);
|
try {
|
||||||
|
SmtpRelay::send($db, $email, $subject, $body, $plain);
|
||||||
|
} catch (SmtpSendException $e) {
|
||||||
|
if ($e->isRecipientRejected()) {
|
||||||
|
// SMTP server does not know this address — roll back the approval
|
||||||
|
// so the user can retry with a valid address.
|
||||||
|
$db->getPDO()->exec(
|
||||||
|
"DELETE FROM file_access_tokens WHERE request_id = {$requestId}"
|
||||||
|
);
|
||||||
|
$db->getPDO()->exec(
|
||||||
|
"UPDATE file_access_requests
|
||||||
|
SET status = 'rejected', admin_notes = 'Adresse e-mail inconnue du serveur de messagerie (550)'
|
||||||
|
WHERE id = {$requestId}"
|
||||||
|
);
|
||||||
|
http_response_code(422);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'message' => "L'adresse e-mail « {$email} » est introuvable sur le serveur de messagerie de l'ERG. Vérifiez l'orthographe ou utilisez une autre adresse.",
|
||||||
|
'status' => 'recipient_rejected',
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
// Transient send failure — access is approved, email may arrive later
|
||||||
|
error_log("[request-access] Email delivery failed for approved request #{$requestId}: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
http_response_code(200);
|
http_response_code(200);
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
@@ -201,6 +238,10 @@ try {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch (SmtpSendException $e) {
|
||||||
|
error_log('Access request SMTP failure: ' . $e->getMessage());
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'message' => 'Erreur lors de l\'envoi de l\'email. Veuillez réessayer.']);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
error_log('Access request failed: ' . $e->getMessage());
|
error_log('Access request failed: ' . $e->getMessage());
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
|||||||
@@ -16,6 +16,31 @@ class SmtpProbeException extends \RuntimeException {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structured exception for SMTP send failures.
|
||||||
|
* Carries the 3-digit SMTP response code and the full server response.
|
||||||
|
*
|
||||||
|
* Notable codes:
|
||||||
|
* 550 / 551 / 553 — recipient address rejected (unknown user, policy, etc.)
|
||||||
|
* 421 / 450 / 451 — transient failures (try again later)
|
||||||
|
* 530 / 535 — authentication failure
|
||||||
|
*/
|
||||||
|
class SmtpSendException extends \RuntimeException {
|
||||||
|
public function __construct(
|
||||||
|
string $message,
|
||||||
|
public readonly int $smtpCode = 0,
|
||||||
|
public readonly string $smtpResponse = ''
|
||||||
|
) {
|
||||||
|
parent::__construct($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the SMTP server permanently rejected the recipient address. */
|
||||||
|
public function isRecipientRejected(): bool
|
||||||
|
{
|
||||||
|
return in_array($this->smtpCode, [550, 551, 552, 553, 554], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SMTP Relay — credentials stored in the DB, sending via a native PHP socket
|
* SMTP Relay — credentials stored in the DB, sending via a native PHP socket
|
||||||
* client (no external dependencies).
|
* client (no external dependencies).
|
||||||
@@ -391,6 +416,9 @@ class SmtpRelay {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return self::smtpSend($s, $to, $rawMessage);
|
return self::smtpSend($s, $to, $rawMessage);
|
||||||
|
} catch (SmtpSendException $e) {
|
||||||
|
error_log('[SmtpRelay] ' . $e->getMessage());
|
||||||
|
throw $e; // propagate structured exception so callers can react
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log('[SmtpRelay] ' . $e->getMessage());
|
error_log('[SmtpRelay] ' . $e->getMessage());
|
||||||
return false;
|
return false;
|
||||||
@@ -463,7 +491,12 @@ class SmtpRelay {
|
|||||||
$expect = function (string $cmd, string $code) use ($send): string {
|
$expect = function (string $cmd, string $code) use ($send): string {
|
||||||
$resp = $send($cmd);
|
$resp = $send($cmd);
|
||||||
if (strncmp($resp, $code, strlen($code)) !== 0) {
|
if (strncmp($resp, $code, strlen($code)) !== 0) {
|
||||||
throw new \RuntimeException("SMTP unexpected response to '{$cmd}': {$resp}");
|
$respCode = (int) substr(trim($resp), 0, 3);
|
||||||
|
throw new SmtpSendException(
|
||||||
|
"SMTP unexpected response to '{$cmd}': " . trim($resp),
|
||||||
|
$respCode,
|
||||||
|
trim($resp)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return $resp;
|
return $resp;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -99,7 +99,13 @@ class StudentEmail {
|
|||||||
$subject = 'Merci — ton TFE a bien été enregistré';
|
$subject = 'Merci — ton TFE a bien été enregistré';
|
||||||
$htmlBody = self::buildHtml($thesis);
|
$htmlBody = self::buildHtml($thesis);
|
||||||
|
|
||||||
$result = SmtpRelay::send($db, $to, $subject, $htmlBody);
|
try {
|
||||||
|
$result = SmtpRelay::send($db, $to, $subject, $htmlBody);
|
||||||
|
} catch (SmtpSendException $e) {
|
||||||
|
// Confirmation email failure must not abort the successful submission.
|
||||||
|
error_log("[StudentEmail] SMTP error sending to {$to} for thesis #{$thesisId}: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
error_log("[StudentEmail] Confirmation sent to {$to} for thesis #{$thesisId}");
|
error_log("[StudentEmail] Confirmation sent to {$to} for thesis #{$thesisId}");
|
||||||
|
|||||||
Reference in New Issue
Block a user