From da53bf5d7a99e9cb73649454443ac4002e57c3b4 Mon Sep 17 00:00:00 2001 From: Pontoporeia Date: Thu, 30 Apr 2026 13:44:59 +0200 Subject: [PATCH] feat: email retry page on 550 rejection; confirmation_email optional in admin form --- TODO.md | 3 + app/public/assets/css/form.css | 101 +++++++++++++++ app/public/partage/index.php | 20 ++- app/public/partage/retry-email.php | 122 ++++++++++++++++++ .../Controllers/ThesisCreateController.php | 13 +- app/src/Dispatcher.php | 7 + app/src/StudentEmail.php | 3 +- app/storage/logs/form-submissions.log | 3 + app/templates/admin/add.php | 2 +- 9 files changed, 260 insertions(+), 14 deletions(-) create mode 100644 app/public/partage/retry-email.php diff --git a/TODO.md b/TODO.md index 1ab7af4..076d76f 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,9 @@ ## Bug fixes - [x] **smtp-test.php** — wrap `SmtpRelay::send()` in `try/catch SmtpSendException` so SMTP delivery failures (e.g. 550 recipient rejected) surface as a proper flash error instead of an uncaught exception/silent crash +- [x] **partage email retry** — on 550 recipient-rejected, redirect to `/partage/retry-email` instead of `recapitulatif`; student can correct address and resend or skip +- [x] **ThesisCreateController** — `confirmation_email` is now optional (empty = skip send) +- [x] **admin/add.php template** — email confirmation field marked optional, label and hint updated ## Previously completed - [x] Multi-file upload for thesis files (basic) diff --git a/app/public/assets/css/form.css b/app/public/assets/css/form.css index ae3d95d..d6dd2fb 100644 --- a/app/public/assets/css/form.css +++ b/app/public/assets/css/form.css @@ -1008,3 +1008,104 @@ a.recap-file-name:hover { .form-help-block ol { margin: 0 0 var(--space-xs); padding-left: var(--space-m); } .form-help-block li { margin-bottom: var(--space-3xs); } .form-help-block a { color: var(--accent-primary); } + +/* ── E-mail retry page ───────────────────────────────────────────────────── */ + +.partage-retry-email { + display: flex; + flex-direction: column; + gap: var(--space-l); + max-width: 600px; + margin: 0 auto; +} + +.retry-email-section { + border-top: 1px solid var(--border-primary); + padding-top: var(--space-m); + display: flex; + flex-direction: column; + gap: var(--space-m); +} + +.retry-email-section h2 { + font-size: var(--step-0); + font-weight: 600; + margin: 0; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-secondary); +} + +.retry-smtp-detail { + display: block; + margin-top: var(--space-2xs); + font-size: var(--step--2); + color: var(--text-secondary); + word-break: break-all; +} + +.retry-email-form { + display: flex; + flex-direction: column; + gap: var(--space-m); +} + +.retry-email-form .field-wrap { + display: flex; + flex-direction: column; + gap: var(--space-2xs); +} + +.retry-email-form label { + font-size: var(--step--1); + font-weight: 600; +} + +.retry-email-form input[type="email"] { + padding: var(--space-xs) var(--space-s); + border: 1px solid var(--border-primary); + border-radius: 4px; + font-size: var(--step-0); + width: 100%; + box-sizing: border-box; +} + +.retry-email-form input.input-error { + border-color: var(--error, #c00); +} + +.retry-email-actions { + display: flex; + gap: var(--space-s); + flex-wrap: wrap; +} + +.btn-primary { + padding: var(--space-xs) var(--space-m); + background: var(--accent-primary); + color: #fff; + border: none; + border-radius: 4px; + font-size: var(--step-0); + cursor: pointer; + font-weight: 600; +} + +.btn-primary:hover { + background: var(--accent-secondary, var(--accent-primary)); + transform: translateY(-1px); +} + +.btn-secondary { + padding: var(--space-xs) var(--space-m); + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border-primary); + border-radius: 4px; + font-size: var(--step-0); + cursor: pointer; +} + +.btn-secondary:hover { + background: color-mix(in srgb, var(--text-secondary) 8%, transparent); +} diff --git a/app/public/partage/index.php b/app/public/partage/index.php index dccc2ac..d1c6db8 100644 --- a/app/public/partage/index.php +++ b/app/public/partage/index.php @@ -447,9 +447,6 @@ function handleShareLinkSubmission(string $slug): void 'share_slug' => $slug, ]); - // Send confirmation e-mail (non-blocking; failure doesn't stop redirect) - $emailSent = StudentEmail::sendConfirmation(Database::getInstance(), $thesisId, $_POST); - // Mark the link as used $shareLinkModel = new ShareLink(Database::getInstance()); $shareLinkModel->incrementUsage($link['id']); @@ -457,7 +454,22 @@ function handleShareLinkSubmission(string $slug): void // Clean up share-specific session data unset($_SESSION[$shareCsrfKey]); unset($_SESSION['share_verified_' . $slug]); - $_SESSION['share_email_sent'] = $emailSent; + + // Send confirmation e-mail — on delivery failure, redirect to retry page + $emailError = null; + try { + $emailSent = StudentEmail::sendConfirmation(Database::getInstance(), $thesisId, $_POST); + $_SESSION['share_email_sent'] = $emailSent; + } catch (SmtpSendException $e) { + if ($e->isRecipientRejected()) { + $_SESSION['share_email_retry_thesis'] = $thesisId; + $_SESSION['share_email_retry_error'] = $e->smtpResponse; + header('Location: /partage/retry-email?id=' . urlencode((string)$thesisId)); + exit(); + } + // Non-recipient errors (relay down, etc.) — skip email silently + $_SESSION['share_email_sent'] = false; + } // Redirect to thanks page header('Location: /partage/recapitulatif?id=' . urlencode((string)$thesisId)); diff --git a/app/public/partage/retry-email.php b/app/public/partage/retry-email.php new file mode 100644 index 0000000..047143e --- /dev/null +++ b/app/public/partage/retry-email.php @@ -0,0 +1,122 @@ + $newEmail]); + unset($_SESSION['share_email_retry_thesis'], $_SESSION['share_email_retry_error']); + $_SESSION['share_email_sent'] = $sent; + header('Location: /partage/recapitulatif?id=' . urlencode((string)$thesisId)); + exit; + } catch (SmtpSendException $e) { + // Update the displayed error with the fresh response + $smtpError = $e->smtpResponse; + $inputError = 'Le serveur a de nouveau rejeté cette adresse. Vérifiez l\'adresse et réessayez.'; + $_SESSION['share_email_retry_error'] = $smtpError; + } + } +} + +$pageTitle = 'Corriger l\'adresse e-mail'; +?> + + + + + + <?= htmlspecialchars($pageTitle) ?> + + + + + + + + + + +
+ +
+

✅ Votre TFE a bien été enregistré !

+

Il y a cependant eu un problème lors de l'envoi de l'e-mail de confirmation.

+
+ +
+

Corriger l'adresse e-mail

+ + + + + + + +

Votre TFE est enregistré — vous pouvez corriger votre adresse ci-dessous pour recevoir le récapitulatif, ou continuer sans e-mail.

+ +
+
+ + +
+
+ + +
+
+
+ +
+ + diff --git a/app/src/Controllers/ThesisCreateController.php b/app/src/Controllers/ThesisCreateController.php index 378b118..4acceb8 100644 --- a/app/src/Controllers/ThesisCreateController.php +++ b/app/src/Controllers/ThesisCreateController.php @@ -325,14 +325,13 @@ class ThesisCreateController } } - // Confirmation e-mail (required) + // Confirmation e-mail (optional) $confirmationEmail = trim($post['confirmation_email'] ?? ''); - if ($confirmationEmail === '') { - throw new Exception("L'adresse e-mail de confirmation est requise."); - } - $confirmationEmail = filter_var($confirmationEmail, FILTER_VALIDATE_EMAIL); - if ($confirmationEmail === false) { - throw new Exception("L'adresse e-mail de confirmation n'est pas valide."); + if ($confirmationEmail !== '') { + $confirmationEmail = filter_var($confirmationEmail, FILTER_VALIDATE_EMAIL); + if ($confirmationEmail === false) { + throw new Exception("L'adresse e-mail de confirmation n'est pas valide."); + } } return compact( diff --git a/app/src/Dispatcher.php b/app/src/Dispatcher.php index 6b5dee1..c076bd5 100644 --- a/app/src/Dispatcher.php +++ b/app/src/Dispatcher.php @@ -122,6 +122,13 @@ class Dispatcher { }; } + // /partage/retry-email (GET: show retry form, POST: resend) + if ($path === '/partage/retry-email') { + return function() { + require APP_ROOT . '/public/partage/retry-email.php'; + }; + } + // /partage/* if (preg_match('#^/partage(/.*)?$#', $path)) { return function() { diff --git a/app/src/StudentEmail.php b/app/src/StudentEmail.php index 12cde81..9e17d3f 100644 --- a/app/src/StudentEmail.php +++ b/app/src/StudentEmail.php @@ -102,9 +102,8 @@ class StudentEmail { 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; + throw $e; // re-throw so callers can react (e.g. redirect to retry page) } if ($result) { diff --git a/app/storage/logs/form-submissions.log b/app/storage/logs/form-submissions.log index f42cb63..a3fe82e 100644 --- a/app/storage/logs/form-submissions.log +++ b/app/storage/logs/form-submissions.log @@ -4,3 +4,6 @@ {"source":"partage","action":"submit","status":"success","thesis_id":18,"identifier":"2025-015","author":"Théo Marchand","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T10:13:43+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":19,"identifier":"2025-016","author":"Lila Dubois, Karim Nassar","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:27:07+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":20,"identifier":"2025-017","author":"Théo Marchand","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:37:11+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":21,"identifier":"2025-018","author":"Théo Marchand","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:41:38+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":22,"identifier":"2025-019","author":"Lila Dubois, Karim Nassar","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:45:36+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":23,"identifier":"2025-020","author":"Zoé Lambert","share_slug":"20260429-DZESJT6X","timestamp":"2026-04-30T11:46: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"} diff --git a/app/templates/admin/add.php b/app/templates/admin/add.php index 4503d51..8a4b41d 100644 --- a/app/templates/admin/add.php +++ b/app/templates/admin/add.php @@ -43,7 +43,7 @@
E-mail de confirmation - +