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
+
+
+ L'adresse e-mail a été rejetée par le serveur.
+
+ = htmlspecialchars($smtpError) ?>
+
+
+
+
+ = htmlspecialchars($inputError) ?>
+
+
+ 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 @@