#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ OmegaKube — Formulaire de contact CGI """ import os, sys, json, time, re, sqlite3, smtplib from urllib.parse import urlparse from datetime import datetime from email.mime.text import MIMEText from email.header import Header sys.stdout.reconfigure(encoding='utf-8') # ─── CONFIGURATION SMTP ──────────────────────────────────────────────────────── # IMPORTANT: Configurez ces paramètres selon votre hébergeur SMTP # # Exemples de configuration pour différents fournisseurs : # # --- Local (Postfix/Exim sur le serveur) --- # SMTP_HOST = "localhost" # SMTP_PORT = 25 # SMTP_SSL = False # SMTP_USER = "" # Pas d'authentification # SMTP_PASSWORD = "" # # --- Gmail --- # SMTP_HOST = "smtp.gmail.com" # SMTP_PORT = 587 # SMTP_SSL = False # Utilise STARTTLS # SMTP_USER = "votre@email.com" # SMTP_PASSWORD = "votre_mot_de_passe_ou_app_password" # # --- SMTP Gmail avec SSL --- # SMTP_HOST = "smtp.gmail.com" # SMTP_PORT = 465 # SMTP_SSL = True # SSL direct # SMTP_USER = "votre@email.com" # SMTP_PASSWORD = "votre_mot_de_passe_ou_app_password" # # --- OVH --- # SMTP_HOST = "ssl0.ovh.net" # ou votre serveur OVH # SMTP_PORT = 587 # SMTP_SSL = False # SMTP_USER = "contact@omegakube.fr" # SMTP_PASSWORD = "votre_mot_de_passe" # # --- Mailjet --- # SMTP_HOST = "in-v3.mailjet.com" # SMTP_PORT = 587 # SMTP_SSL = False # SMTP_USER = "votre_api_key" # SMTP_PASSWORD = "votre_api_secret" # # --- SendGrid --- # SMTP_HOST = "smtp.sendgrid.net" # SMTP_PORT = 587 # SMTP_SSL = False # SMTP_USER = "apikey" # SMTP_PASSWORD = "votre_sendgrid_api_key" # # Pour tester la connexion SMTP, exécutez : # python3 -c "import smtplib; srv = smtplib.SMTP('localhost', 25); srv.quit(); print('OK')" TO_EMAIL = "contact@omegakube.fr" FROM_EMAIL = "noreply@omegakube.fr" SMTP_HOST = "localhost" # ⚠️ À MODIFIER selon votre hébergeur SMTP_PORT = 25 # ⚠️ Port SMTP (25, 465, 587) SMTP_SSL = False # True pour SSL direct (port 465), False pour STARTTLS (port 587) SMTP_USER = "" # ⚠️ Nom d'utilisateur si authentification requise SMTP_PASSWORD = "" # ⚠️ Mot de passe si authentification requise # ────────────────────────────────────────────────────────────────────────────── def cgi_response(status_code, status_text, body, extra_headers=None): """Émet une réponse CGI complète (Status en premier, ligne vide obligatoire).""" print(f"Status: {status_code} {status_text}") if extra_headers: for h in extra_headers: print(h) print("Content-Type: application/json; charset=utf-8") print() print(json.dumps(body, ensure_ascii=False)) sys.exit(0) # ─── CORS ───────────────────────────────────────────────────────────────────── ALLOWED_ORIGINS = ["https://omegakube.fr", "https://www.omegakube.fr"] origin = os.environ.get("HTTP_ORIGIN", "") referer = os.environ.get("HTTP_REFERER", "") if origin: if origin not in ALLOWED_ORIGINS: cgi_response(403, "Forbidden", {"success": False, "error": "Origine non autorisée"}) cors_origin = origin elif referer: host = urlparse(referer).netloc if host not in ("omegakube.fr", "www.omegakube.fr"): cgi_response(403, "Forbidden", {"success": False, "error": "Référent non autorisé"}) cors_origin = "https://omegakube.fr" else: cors_origin = "https://omegakube.fr" # curl/test direct → on accepte cors_headers = [ f"Access-Control-Allow-Origin: {cors_origin}", "Access-Control-Allow-Methods: POST, OPTIONS", "Access-Control-Allow-Headers: Content-Type", ] # ─── PREFLIGHT OPTIONS ──────────────────────────────────────────────────────── method = os.environ.get("REQUEST_METHOD", "GET") if method == "OPTIONS": print("Status: 204 No Content") for h in cors_headers: print(h) print() sys.exit(0) if method != "POST": cgi_response(405, "Method Not Allowed", {"success": False, "error": "Méthode POST requise"}, cors_headers) # ─── IP CLIENT ──────────────────────────────────────────────────────────────── ip = (os.environ.get("HTTP_CF_CONNECTING_IP") or os.environ.get("HTTP_X_FORWARDED_FOR", "").split(",")[0].strip() or os.environ.get("REMOTE_ADDR", "unknown")) # ─── RATE LIMITING ──────────────────────────────────────────────────────────── DB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "rate_limits") DB_FILE = os.path.join(DB_DIR, "rate_limit.db") def check_rate_limit(client_ip): try: os.makedirs(DB_DIR, exist_ok=True) conn = sqlite3.connect(DB_FILE) cur = conn.cursor() cur.execute("CREATE TABLE IF NOT EXISTS submits (ip TEXT, ts REAL)") conn.commit() now = time.time() cur.execute("DELETE FROM submits WHERE ts < ?", (now - 60,)) conn.commit() cur.execute("SELECT COUNT(*) FROM submits WHERE ip = ?", (client_ip,)) if cur.fetchone()[0] >= 3: conn.close() return False, "Trop de requêtes. Attendez une minute avant de réessayer." cur.execute("SELECT MAX(ts) FROM submits WHERE ip = ?", (client_ip,)) last = cur.fetchone()[0] if last and (now - last) < 5: conn.close() return False, "Veuillez patienter quelques secondes entre chaque envoi." cur.execute("INSERT INTO submits (ip, ts) VALUES (?, ?)", (client_ip, now)) conn.commit() conn.close() return True, "" except Exception: return True, "" # SQLite indisponible → on laisse passer ok, msg = check_rate_limit(ip) if not ok: cgi_response(429, "Too Many Requests", {"success": False, "error": msg}, cors_headers) # ─── LECTURE JSON ───────────────────────────────────────────────────────────── try: length = int(os.environ.get("CONTENT_LENGTH", 0)) data = json.loads(sys.stdin.read(length) if length > 0 else "{}") except Exception: cgi_response(400, "Bad Request", {"success": False, "error": "Données JSON invalides"}, cors_headers) if not data: cgi_response(400, "Bad Request", {"success": False, "error": "Aucune donnée reçue"}, cors_headers) # ─── VALIDATION ─────────────────────────────────────────────────────────────── def clean(s, max_len=None): s = re.sub(r"[\r\n]|%0[adAD]", "", str(s)).strip() return s[:max_len] if max_len else s # Honeypot if str(data.get("website", "")): cgi_response(200, "OK", {"success": True, "message": "Message envoyé !"}, cors_headers) name = clean(data.get("name", ""), 100) email = clean(data.get("email", ""), 254).lower() subject = clean(data.get("subject", ""), 150) message = str(data.get("message", ""))[:5000].strip() errors = [] if len(name) < 2: errors.append("Le nom doit contenir au moins 2 caractères.") if not re.match(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", email): errors.append("L'adresse email n'est pas valide.") if len(message) < 10: errors.append("Le message doit contenir au moins 10 caractères.") if errors: cgi_response(400, "Bad Request", {"success": False, "errors": errors}, cors_headers) # ─── EMAIL ──────────────────────────────────────────────────────────────────── sep = "=" * 40 subject_line = f"[OmegaKube] {name} — {datetime.now().strftime('%d/%m/%Y %H:%M')}" if subject: subject_line += f" ({subject})" body = f"OmegaKube — Nouveau message de contact\n{sep}\n\n" body += f"Nom : {name}\nEmail : {email}\nObjet : {subject or 'Non précisé'}\n\n" body += f"{sep}\n\nMESSAGE :\n\n{message}\n\n{sep}\n\n" body += f"Répondre : {email}\nReçu le : {datetime.now().strftime('%d/%m/%Y %H:%M')}\n" body += f"IP : {ip}\n\n{sep}\nOmegaKube — omegakube.fr\n" def send_smtp(to_addr, subj, content, from_addr, reply_to): """Envoie un email via SMTP avec gestion d'erreur détaillée.""" msg = MIMEText(content, "plain", "utf-8") msg["Subject"] = Header(subj, "utf-8") msg["From"] = f"OmegaKube <{from_addr}>" msg["To"] = to_addr msg["Reply-To"] = reply_to srv = None try: # Log de tentative de connexion print(f"[send_mail] Tentative de connexion SMTP: host={SMTP_HOST}, port={SMTP_PORT}, ssl={SMTP_SSL}, user={bool(SMTP_USER)}", file=sys.stderr) if SMTP_SSL: srv = smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=10) else: srv = smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) srv.starttls() # Authentification si nécessaire if SMTP_USER and SMTP_PASSWORD: print(f"[send_mail] Authentification SMTP...", file=sys.stderr) srv.login(SMTP_USER, SMTP_PASSWORD) print(f"[send_mail] Envoi de l'email à: {to_addr}", file=sys.stderr) srv.sendmail(from_addr, [to_addr], msg.as_string()) srv.quit() print(f"[send_mail] Email envoyé avec succès !", file=sys.stderr) return True, None except smtplib.SMTPConnectError as e: error_msg = f"Impossible de se connecter au serveur SMTP. Vérifiez la configuration: host={SMTP_HOST}, port={SMTP_PORT}" print(f"[send_mail] SMTPConnectError: {e}", file=sys.stderr) return False, error_msg except smtplib.SMTPAuthenticationError as e: error_msg = "Authentification SMTP échouée. Vérifiez SMTP_USER et SMTP_PASSWORD." print(f"[send_mail] SMTPAuthenticationError: {e}", file=sys.stderr) return False, error_msg except smtplib.SMTPException as e: error_msg = f"Erreur SMTP: {str(e)}" print(f"[send_mail] SMTPException: {e}", file=sys.stderr) return False, error_msg except TimeoutError as e: error_msg = f"Timeout lors de la connexion au serveur SMTP ({SMTP_HOST}:{SMTP_PORT}). Vérifiez que le serveur est accessible." print(f"[send_mail] TimeoutError: {e}", file=sys.stderr) return False, error_msg except Exception as e: error_msg = f"Erreur inattendue lors de l'envoi: {str(e)}" print(f"[send_mail] Exception: {e}", file=sys.stderr) return False, error_msg finally: if srv: try: srv.quit() except: pass # ─── TEST DE CONNEXION SMTP (optionnel) ──────────────────────────────────────── def test_smtp_connection(): """Teste la connexion SMTP avant l'envoi (pour le débogage).""" try: if SMTP_SSL: srv = smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, timeout=5) else: srv = smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=5) srv.starttls() if SMTP_USER and SMTP_PASSWORD: srv.login(SMTP_USER, SMTP_PASSWORD) srv.quit() return True except Exception as e: print(f"[send_mail] TEST SMTP échoué: {e}", file=sys.stderr) return False # ─── RÉPONSE ────────────────────────────────────────────────────────────────── success, error_msg = send_smtp(TO_EMAIL, subject_line, body, FROM_EMAIL, email) if success: cgi_response(200, "OK", {"success": True, "message": "Votre message a bien été envoyé ! Nous vous répondrons rapidement."}, cors_headers) else: # Message d'erreur détaillé mais sécurisé (pas d'info sensible) user_error = error_msg or "Erreur lors de l'envoi. Vérifiez la configuration SMTP ou contactez-nous directement sur Discord." cgi_response(500, "Internal Server Error", {"success": False, "error": user_error}, cors_headers)