diff --git a/api/contact.php b/api/contact.php index 744f30e..47e0656 100644 --- a/api/contact.php +++ b/api/contact.php @@ -1,29 +1,26 @@ false, 'error' => 'Méthode non autorisée']); exit; } -// Récupérer les données JSON $input = json_decode(file_get_contents('php://input'), true); if (!$input) { @@ -33,29 +30,23 @@ if (!$input) { } try { - // 1. Valider le token CSRF if (!verifyCsrfToken($input['csrf_token'] ?? '')) { throw new Exception('Token de sécurité invalide. Veuillez rafraîchir la page.'); } - // 2. Vérifier reCAPTCHA $recaptchaScore = verifyRecaptcha($input['recaptcha_token'] ?? ''); if ($recaptchaScore < RECAPTCHA_THRESHOLD) { error_log("reCAPTCHA score trop bas: {$recaptchaScore}"); throw new Exception('Vérification anti-spam échouée. Veuillez réessayer.'); } - // 3. Valider et nettoyer les données $data = validateContactData($input); - // 4. Envoyer l'email $sent = sendContactEmail($data); - if (!$sent) { throw new Exception('Erreur lors de l\'envoi du message. Veuillez réessayer plus tard.'); } - // 5. Succès echo json_encode([ 'success' => true, 'message' => 'Votre message a bien été envoyé ! Je vous répondrai dans les meilleurs délais.' diff --git a/api/contact.php.old b/api/contact.php.old new file mode 100644 index 0000000..744f30e --- /dev/null +++ b/api/contact.php.old @@ -0,0 +1,70 @@ + false, 'error' => 'Méthode non autorisée']); + exit; +} + +// Récupérer les données JSON +$input = json_decode(file_get_contents('php://input'), true); + +if (!$input) { + http_response_code(400); + echo json_encode(['success' => false, 'error' => 'Données invalides']); + exit; +} + +try { + // 1. Valider le token CSRF + if (!verifyCsrfToken($input['csrf_token'] ?? '')) { + throw new Exception('Token de sécurité invalide. Veuillez rafraîchir la page.'); + } + + // 2. Vérifier reCAPTCHA + $recaptchaScore = verifyRecaptcha($input['recaptcha_token'] ?? ''); + if ($recaptchaScore < RECAPTCHA_THRESHOLD) { + error_log("reCAPTCHA score trop bas: {$recaptchaScore}"); + throw new Exception('Vérification anti-spam échouée. Veuillez réessayer.'); + } + + // 3. Valider et nettoyer les données + $data = validateContactData($input); + + // 4. Envoyer l'email + $sent = sendContactEmail($data); + + if (!$sent) { + throw new Exception('Erreur lors de l\'envoi du message. Veuillez réessayer plus tard.'); + } + + // 5. Succès + echo json_encode([ + 'success' => true, + 'message' => 'Votre message a bien été envoyé ! Je vous répondrai dans les meilleurs délais.' + ]); + +} catch (Exception $e) { + http_response_code(400); + echo json_encode([ + 'success' => false, + 'error' => $e->getMessage() + ]); +} diff --git a/assets/css/input.css b/assets/css/input.css index b11bead..def3246 100644 --- a/assets/css/input.css +++ b/assets/css/input.css @@ -99,7 +99,7 @@ .input { @apply w-full px-4 py-3 bg-surface border border-border rounded-lg - text-text-primary placeholder-text-muted + text-text-muted placeholder-text-muted transition-all duration-150 focus:outline-none focus:border-primary focus:shadow-input-focus; } diff --git a/composer.json b/composer.json index 130b9da..ca5c0be 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "type": "project", "require": { "php": ">=8.0", - "vlucas/phpdotenv": "^5.6" + "vlucas/phpdotenv": "^5.6", + "phpmailer/phpmailer": "^7.0" } } diff --git a/composer.lock b/composer.lock index bd32a60..f707931 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "adbddd7a48b14ed78896b2d6c5ef28e9", + "content-hash": "ef9466a44690e608fe2d148c314ef38c", "packages": [ { "name": "graham-campbell/result-type", @@ -68,6 +68,88 @@ ], "time": "2025-12-27T19:43:20+00:00" }, + { + "name": "phpmailer/phpmailer", + "version": "v7.0.2", + "source": { + "type": "git", + "url": "https://github.com/PHPMailer/PHPMailer.git", + "reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088", + "reference": "ebf1655bd5b99b3f97e1a3ec0a69e5f4cd7ea088", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "php": ">=5.5.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "doctrine/annotations": "^1.2.6 || ^1.13.3", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "squizlabs/php_codesniffer": "^3.13.5", + "yoast/phpunit-polyfills": "^1.0.4" + }, + "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", + "directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example", + "ext-imap": "Needed to support advanced email address parsing according to RFC822", + "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", + "ext-openssl": "Needed for secure SMTP sending and DKIM signing", + "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", + "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", + "league/oauth2-google": "Needed for Google XOAUTH2 authentication", + "psr/log": "For optional PSR-3 debug logging", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPMailer\\PHPMailer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-only" + ], + "authors": [ + { + "name": "Marcus Bointon", + "email": "phpmailer@synchromedia.co.uk" + }, + { + "name": "Jim Jagielski", + "email": "jimjag@gmail.com" + }, + { + "name": "Andy Prevost", + "email": "codeworxtech@users.sourceforge.net" + }, + { + "name": "Brent R. Matzelle" + } + ], + "description": "PHPMailer is a full-featured email creation and transfer class for PHP", + "support": { + "issues": "https://github.com/PHPMailer/PHPMailer/issues", + "source": "https://github.com/PHPMailer/PHPMailer/tree/v7.0.2" + }, + "funding": [ + { + "url": "https://github.com/Synchro", + "type": "github" + } + ], + "time": "2026-01-09T18:02:33+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.5", diff --git a/includes/config.php b/includes/config.php index e32e2ec..9d948ae 100644 --- a/includes/config.php +++ b/includes/config.php @@ -55,5 +55,17 @@ define('RECAPTCHA_THRESHOLD', 0.5); // Score minimum (0.0 à 1.0) // Contact define('CONTACT_EMAIL', $_ENV['CONTACT_EMAIL'] ?? ''); +// SMTP +define('SMTP_HOST', $_ENV['SMTP_HOST'] ?? '127.0.0.1'); +define('SMTP_PORT', (int) $_ENV['SMTP_PORT'] ?? '1025'); +define('SMTP_USERNAME', $_ENV['SMTP_USERNAME'] ?? ''); +define('SMTP_PASSWORD', $_ENV['SMTP_PASSWORD'] ?? ''); +define('SMTP_ENCRYPTION', $_ENV['SMTP_ENCRYPTION'] ?? 'none'); // none|tls|ssl + +define('MAIL_FROM_ADDRESS', $_ENV['MAIL_FROM_ADDRESS'] ?? SMTP_USERNAME); +define('MAIL_FROM_NAME', $_ENV['MAIL_FROM_NAME'] ?? 'Portfolio - Contact'); + // Sécurité define('APP_SECRET', $_ENV['APP_SECRET'] ?? ''); + + diff --git a/includes/functions.php b/includes/functions.php index bd64a46..1e18796 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -3,6 +3,11 @@ * Fonctions helpers du portfolio */ +require_once __DIR__ . '/../vendor/autoload.php'; + +use PHPMailer\PHPMailer\PHPMailer; +use PHPMailer\PHPMailer\Exception as MailerException; + /** * Inclut un template avec des données * @param string $name Nom du template (sans .php) @@ -410,6 +415,127 @@ function validateContactData(array $input): array * @return bool True si envoyé avec succès */ function sendContactEmail(array $data): bool +{ + // Vérifier config minimale + if (!defined('CONTACT_EMAIL') || empty(CONTACT_EMAIL)) { + error_log('CONTACT_EMAIL non configuré'); + return false; + } + if (!defined('SMTP_HOST') || !defined('SMTP_PORT')) { + error_log('SMTP_HOST/SMTP_PORT non configurés'); + return false; + } + + // Autoload PHPMailer (Composer) + $autoload = __DIR__ . '/../vendor/autoload.php'; + if (!file_exists($autoload)) { + error_log('PHPMailer autoload introuvable. As-tu installé Composer ?'); + return false; + } + require_once $autoload; + + $categorieLabels = [ + 'projet' => 'Projet freelance', + 'poste' => 'Proposition de poste', + 'autre' => 'Autre demande' + ]; + + $categorie = $categorieLabels[$data['categorie']] ?? 'Autre'; + $entreprise = $data['entreprise'] ?: 'Non renseignée'; + + $subject = "[Portfolio] {$categorie} - {$data['objet']}"; + + $body = "═══════════════════════════════════════════\n"; + $body .= "NOUVEAU MESSAGE - PORTFOLIO\n"; + $body .= "═══════════════════════════════════════════\n\n"; + $body .= "DE: {$data['prenom']} {$data['nom']}\n"; + $body .= "EMAIL: {$data['email']}\n"; + $body .= "ENTREPRISE: {$entreprise}\n"; + $body .= "CATÉGORIE: {$categorie}\n\n"; + $body .= "───────────────────────────────────────────\n"; + $body .= "OBJET: {$data['objet']}\n"; + $body .= "───────────────────────────────────────────\n\n"; + $body .= "MESSAGE:\n\n"; + $body .= "{$data['message']}\n\n"; + $body .= "═══════════════════════════════════════════\n"; + $body .= "Envoyé le {$data['date']}\n"; + $body .= "IP: {$data['ip']}\n"; + $body .= "═══════════════════════════════════════════"; + + $mail = new PHPMailer(true); + + try { + // Mode SMTP + $mail->isSMTP(); + $mail->Host = SMTP_HOST; + $mail->Port = SMTP_PORT; + + // Auth SMTP si configurée + $hasAuth = !empty(SMTP_USERNAME) && !empty(SMTP_PASSWORD); + $mail->SMTPAuth = $hasAuth; + + if ($hasAuth) { + $mail->Username = SMTP_USERNAME; + $mail->Password = SMTP_PASSWORD; + } + + // Encryption + $enc = strtolower((string) SMTP_ENCRYPTION); + if ($enc === 'tls') { + $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; + } elseif ($enc === 'ssl') { + $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; + } else { + // none + $mail->SMTPSecure = false; + $mail->SMTPAutoTLS = false; + } + + $mail->CharSet = 'UTF-8'; + $mail->Encoding = 'base64'; + + // Expéditeur (doit être un alias autorisé si Proton) + $fromAddress = defined('MAIL_FROM_ADDRESS') ? MAIL_FROM_ADDRESS : SMTP_USERNAME; + $fromName = defined('MAIL_FROM_NAME') ? MAIL_FROM_NAME : 'Portfolio - Contact'; + + if (empty($fromAddress)) { + error_log('MAIL_FROM_ADDRESS/SMTP_USERNAME vide : impossible de définir From'); + return false; + } + + $mail->setFrom($fromAddress, $fromName); + + // Reply-To = email du visiteur + if (!empty($data['email'])) { + $mail->addReplyTo($data['email'], trim(($data['prenom'] ?? '') . ' ' . ($data['nom'] ?? ''))); + } + + // Destinataire + $mail->addAddress(CONTACT_EMAIL); + + $mail->Subject = $subject; + $mail->Body = $body; + $mail->isHTML(false); + + $mail->SMTPDebug = 2; + $mail->Debugoutput = function ($str, $level) { + error_log("PHPMailer debug($level): $str"); + }; + + + // Timeouts/log (utile en debug) + $mail->Timeout = 10; + + $mail->send(); + return true; + + } catch (\Throwable $e) { + error_log('Mail send unexpected error: ' . $e->getMessage()); + return false; + } +} + +/*function sendContactEmail(array $data): bool { $categorieLabels = [ 'projet' => 'Projet freelance', @@ -460,4 +586,4 @@ function sendContactEmail(array $data): bool } return $result; -} +}*/ diff --git a/pages/contact.php b/pages/contact.php index 20bff8e..ef2d883 100644 --- a/pages/contact.php +++ b/pages/contact.php @@ -47,7 +47,7 @@ include_template('navbar', compact('currentPage')); type="text" id="nom" name="nom" - class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" + class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" required maxlength="100" autocomplete="family-name" @@ -65,7 +65,7 @@ include_template('navbar', compact('currentPage')); type="text" id="prenom" name="prenom" - class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" + class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" required maxlength="100" autocomplete="given-name" @@ -86,7 +86,7 @@ include_template('navbar', compact('currentPage')); type="email" id="email" name="email" - class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" + class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" required maxlength="255" autocomplete="email" @@ -104,7 +104,7 @@ include_template('navbar', compact('currentPage')); type="text" id="entreprise" name="entreprise" - class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" + class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" maxlength="200" autocomplete="organization" placeholder="Nom de votre entreprise" @@ -120,13 +120,13 @@ include_template('navbar', compact('currentPage')); @@ -140,7 +140,7 @@ include_template('navbar', compact('currentPage')); type="text" id="objet" name="objet" - class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-primary placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" + class="w-full px-4 py-3 bg-surface-alt border border-border rounded-lg text-text-muted placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary transition-colors" required maxlength="200" placeholder="Résumez votre demande en quelques mots" @@ -156,7 +156,7 @@ include_template('navbar', compact('currentPage'));