added notification and cancellation mail

This commit is contained in:
Claudio 2025-10-08 17:33:26 +02:00
parent 8c3c3982ac
commit 26fb165c98
18 changed files with 1221 additions and 365 deletions

2
.env
View File

@ -1,5 +1,5 @@
APP_ENV=production APP_ENV=production
APP_DEBUG=false APP_DEBUG=true
APP_KEY=base64:aj3bR0zA9I8nZ1Rm5alncE4QFTPNoHVkd8YSRJEImwY= APP_KEY=base64:aj3bR0zA9I8nZ1Rm5alncE4QFTPNoHVkd8YSRJEImwY=
APP_URL=https://yogibook.yogasoul.it APP_URL=https://yogibook.yogasoul.it

View File

@ -0,0 +1,139 @@
<?php
// Abilita visualizzazione errori PHP
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
// Connessione al database
include('include/headscript.php');
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connessione al database fallita: " . $conn->connect_error);
}
// Recupera parametri GET: idbookingclass e token
if (isset($_GET['idbookingclass']) && isset($_GET['token'])) {
$idbookingclass = $_GET['idbookingclass'];
$token = $_GET['token'];
// Verifica validità: token corrisponde, lezione futura e prima delle 12:00 del giorno
$query = "SELECT * FROM bookingclass
WHERE idbookingclass = ?
AND cancellation_token = ?
AND status = 'booked'
AND bookingstart > NOW()
AND NOW() <= DATE_ADD(DATE(bookingstart), INTERVAL 12 HOUR)";
$stmt = $conn->prepare($query);
if ($stmt) {
$stmt->bind_param("is", $idbookingclass, $token);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
$bookingstart = $row['bookingstart'];
$newtimeformat = date("d-m-Y H:i", strtotime($bookingstart));
// Aggiorna status a 'cancelled' e invalida token
$updateQuery = "UPDATE bookingclass
SET status = 'cancelled', cancellation_token = NULL
WHERE idbookingclass = ?";
$updateStmt = $conn->prepare($updateQuery);
$updateStmt->bind_param("i", $idbookingclass);
$updateStmt->execute();
// Recupera dati utente e servizio
$dataQuery = "SELECT bookingclass.*, auth_users.*, service.*
FROM bookingclass
LEFT JOIN auth_users ON bookingclass.iduser = auth_users.id
LEFT JOIN service ON bookingclass.idservice = service.idservice
WHERE bookingclass.idbookingclass = ?";
$dataStmt = $conn->prepare($dataQuery);
$dataStmt->bind_param("i", $idbookingclass);
$dataStmt->execute();
$dataResult = $dataStmt->get_result();
$dataRow = $dataResult->fetch_assoc();
$emailuser = $dataRow['email'];
$firstname = $dataRow['first_name'] ?? 'Utente';
// Prepara messaggio email
$messagecancel = "<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 18px; line-height: 34.2px;'><strong><span style='line-height: 34.2px; font-size: 18px;'> Ciao $firstname , </span></strong></span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>La tua lezione è stata cancellata con successo! </span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Dettaglio: $newtimeformat</span></p>
<br>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Per vedere e gestire le tue lezioni clicca qui: https://yogibook.yogasoul.it </span></p>
<br>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Ci vediamo sul tappetino!</span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Il Team Yogasoul</span></p>";
// Definisci $messageedit per il template
$messageedit = $messagecancel;
// Definisci $buttonedit (pulsante generico)
$buttonedit = "<a href='https://yogibook.yogasoul.it/' target='_blank' class='v-button v-font-size' style='box-sizing: border-box;display: inline-block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFFFFF; background-color: #3AAEE0; border-radius: 4px;-webkit-border-radius: 4px; -moz-border-radius: 4px; width:auto; max-width:100%; overflow-wrap: break-word; word-break: break-word; word-wrap:break-word; mso-border-alt: none;font-size: 14px;'>
<span style='display:block;padding:10px 20px;line-height:120%;'><span style='line-height: 16.8px;'>YogiBook - YogaSoul</span></span>
</a>";
require 'phpmailer/src/Exception.php';
require 'phpmailer/src/PHPMailer.php';
require 'phpmailer/src/SMTP.php';
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'mail.yogasoul.it';
$mail->SMTPAuth = true;
$mail->Username = 'info@yogasoul.it';
$mail->Password = '!Testolina88';
$mail->SMTPSecure = 'tls';
$mail->Port = 587;
// Verifica esistenza file template
if (!file_exists('mail/emailtemplate2.php')) {
throw new Exception("File emailtemplate2.php non trovato.");
}
include('mail/emailtemplate2.php');
// Verifica che $mailmessage1 esista
if (!isset($mailmessage1)) {
throw new Exception("Variabile \$mailmessage1 non definita in emailtemplate2.php.");
}
// Sostituisci placeholder (per compatibilità)
$htmlContent = str_replace('{message}', $messagecancel, $mailmessage1);
$mail->From = 'info@yogasoul.it';
$mail->FromName = 'YogiBook [YogaSoul]';
$mail->addAddress($emailuser);
$mail->Subject = "YogiBook - Lezione cancellata con successo!";
$mail->Body = $htmlContent;
$mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
$mail->send();
// Mostra landing di conferma
echo "<h1>Cancellazione confermata</h1>";
echo "<p>La lezione del $newtimeformat è stata cancellata con successo.</p>";
echo "<a href='https://yogibook.yogasoul.it'>Torna al portale</a>";
} catch (Exception $e) {
echo "Errore invio email: " . $mail->ErrorInfo;
}
} else {
echo "Link non valido o scaduto.";
}
$stmt->close();
} else {
echo "Errore nella preparazione della query: " . $conn->error;
}
} else {
echo "Parametri mancanti.";
}
$conn->close();

View File

@ -0,0 +1,26 @@
<?php
require_once('include/headscript.php');
$conn = mysqli_connect($servername, $username, $password, $dbname);
if (!$conn) {
die(json_encode(['error' => 'Connessione al database fallita']));
}
// Query per ottenere tutte le classi disponibili
$sql = "SELECT id, servicename, day, time FROM classes ORDER BY servicename, day, time";
$result = mysqli_query($conn, $sql);
$classes = [];
while ($row = mysqli_fetch_assoc($result)) {
$classes[] = [
'id' => $row['id'],
'servicename' => $row['servicename'],
'day' => $row['day'],
'time' => $row['time']
];
}
mysqli_close($conn);
header('Content-Type: application/json');
echo json_encode($classes);

View File

@ -0,0 +1,35 @@
<?php
require_once('include/headscript.php');
$conn = mysqli_connect($servername, $username, $password, $dbname);
if (!$conn) {
die(json_encode(['error' => 'Connessione al database fallita']));
}
$class_id = isset($_POST['class_id']) ? intval($_POST['class_id']) : 0;
if ($class_id <= 0) {
die(json_encode(['error' => 'ID classe non valido']));
}
// Query per ottenere le date disponibili per la classe specificata
// Supponiamo che ci sia una tabella 'class_schedule' con le date disponibili
$sql = "SELECT DISTINCT DATE(bookingstart) as available_date
FROM class_schedule
WHERE class_id = ?
ORDER BY available_date";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'i', $class_id);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$availableDates = [];
while ($row = mysqli_fetch_assoc($result)) {
$availableDates[] = $row['available_date'];
}
mysqli_stmt_close($stmt);
mysqli_close($conn);
header('Content-Type: application/json');
echo json_encode(['availableDates' => $availableDates]);

162
public/promemoria-cron.php Normal file
View File

@ -0,0 +1,162 @@
<?php
// Abilita visualizzazione errori PHP
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
include('include/headscript.php');
// Verifica connessione al database
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connessione al database fallita: " . $conn->connect_error);
}
// Inizializza contatore e log
$emailCount = 0;
$errors = [];
$logFile = 'promemoria_cron_log.txt';
$logMessage = "Esecuzione cron: " . date('Y-m-d H:i:s') . "\n";
// Seleziona prenotazioni per domani
$tomorrow = date('Y-m-d', strtotime('+1 day'));
$query = "SELECT bc.*, au.email, au.first_name, s.servicename
FROM bookingclass bc
LEFT JOIN auth_users au ON bc.iduser = au.id
LEFT JOIN service s ON bc.idservice = s.idservice
WHERE DATE(bc.bookingstart) = ? AND bc.status = 'booked'";
$stmt = $conn->prepare($query);
if (!$stmt) {
$errors[] = "Errore preparazione query: " . $conn->error;
file_put_contents($logFile, $logMessage . "Errore query: " . $conn->error . "\n", FILE_APPEND);
echo "Errore preparazione query: " . $conn->error;
exit;
}
$stmt->bind_param("s", $tomorrow);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
$logMessage .= "Nessuna prenotazione trovata per domani.\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
echo "Nessuna prenotazione trovata per domani.\n";
exit;
}
while ($row = $result->fetch_assoc()) {
$idbookingclass = $row['idbookingclass'];
$token = $row['cancellation_token'];
// Genera token se assente
if (empty($token)) {
$token = bin2hex(random_bytes(32));
$updateQuery = "UPDATE bookingclass SET cancellation_token = ? WHERE idbookingclass = ?";
$updateStmt = $conn->prepare($updateQuery);
if ($updateStmt) {
$updateStmt->bind_param("si", $token, $idbookingclass);
$updateStmt->execute();
} else {
$errors[] = "Errore preparazione query token per ID $idbookingclass: " . $conn->error;
}
}
$firstname = $row['first_name'] ?? 'Utente';
$emailuser = $row['email'];
$servicename = $row['servicename'] ?? 'Sconosciuta';
$bookingstart = $row['bookingstart'];
$dataformat = date("d-m-Y H:i", strtotime($bookingstart));
// Verifica email valida
if (!filter_var($emailuser, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Email non valida per ID $idbookingclass: $emailuser";
$logMessage .= "Email non valida per ID $idbookingclass: $emailuser\n";
continue;
}
// Link cancellazione
$link = "https://yogibook.yogasoul.it/cancella-prenotazione.php?idbookingclass=$idbookingclass&token=$token";
// Messaggio email
$message = "<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 18px; line-height: 34.2px;'><strong>Ciao $firstname,</strong></span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Promemoria: domani hai la lezione $servicename del $dataformat.</span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Puoi cancellarla fino alle 12:00 cliccando qui:</span></p>
<a href='$link' target='_blank'>Cancella prenotazione</a>
<br>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Ci vediamo sul tappetino!</span></p>
<p style='font-size: 14px; line-height: 190%;'><span style='font-size: 16px; line-height: 30.4px;'>Il Team Yogasoul</span></p>";
// Definisci $messageedit per il template
$messageedit = $message;
// Definisci $buttonedit (vuoto o pulsante generico)
$buttonedit = "<a href='https://yogibook.yogasoul.it/' target='_blank' class='v-button v-font-size' style='box-sizing: border-box;display: inline-block;text-decoration: none;-webkit-text-size-adjust: none;text-align: center;color: #FFFFFF; background-color: #3AAEE0; border-radius: 4px;-webkit-border-radius: 4px; -moz-border-radius: 4px; width:auto; max-width:100%; overflow-wrap: break-word; word-break: break-word; word-wrap:break-word; mso-border-alt: none;font-size: 14px;'>
<span style='display:block;padding:10px 20px;line-height:120%;'><span style='line-height: 16.8px;'>YogiBook - YogaSoul</span></span>
</a>";
require 'phpmailer/src/Exception.php';
require 'phpmailer/src/PHPMailer.php';
require 'phpmailer/src/SMTP.php';
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'mail.yogasoul.it';
$mail->SMTPAuth = true;
$mail->Username = 'info@yogasoul.it';
$mail->Password = '!Testolina88';
$mail->SMTPSecure = 'tls';
$mail->Port = 587;
// Verifica esistenza file template
if (!file_exists('mail/emailtemplate2.php')) {
throw new Exception("File emailtemplate2.php non trovato.");
}
include('mail/emailtemplate2.php');
// Verifica che $mailmessage1 esista
if (!isset($mailmessage1)) {
throw new Exception("Variabile \$mailmessage1 non definita in emailtemplate2.php.");
}
// Sostituisci placeholder (anche se non usato, per compatibilità)
$htmlContent = str_replace('{message}', $message, $mailmessage1);
$mail->From = 'info@yogasoul.it';
$mail->FromName = 'YogiBook [YogaSoul]';
$mail->addAddress($emailuser);
$mail->Subject = "YogiBook - Promemoria lezione domani!";
$mail->Body = $htmlContent;
$mail->AltBody = 'Promemoria lezione.';
$mail->send();
$emailCount++;
$logMessage .= "Email inviata a $emailuser per lezione ID $idbookingclass ($dataformat)\n";
} catch (Exception $e) {
$errors[] = "Errore invio email a $emailuser (ID $idbookingclass): " . $mail->ErrorInfo;
$logMessage .= "Errore invio a $emailuser (ID $idbookingclass): " . $mail->ErrorInfo . "\n";
}
sleep(2); // Delay 2 secondi
}
// Scrivi log
file_put_contents($logFile, $logMessage, FILE_APPEND);
// Output debug
echo "Esecuzione completata: $emailCount email inviate.\n";
if (!empty($errors)) {
echo "Errori rilevati:\n";
foreach ($errors as $error) {
echo "- $error\n";
}
} else {
echo "Nessun errore.\n";
}
echo "Dettagli nel file di log: $logFile\n";
$conn->close();

View File

@ -0,0 +1,4 @@
Esecuzione cron: 2025-10-08 13:39:42
Esecuzione cron: 2025-10-08 13:40:48
Esecuzione cron: 2025-10-08 13:54:46
Email inviata a info@claudiosironi.com per lezione ID 8 (09-10-2025 18:15)

View File

@ -0,0 +1,57 @@
<?php
require_once('include/headscript.php');
$conn = mysqli_connect($servername, $username, $password, $dbname);
if (!$conn) {
die(json_encode(['success' => false, 'message' => 'Connessione al database fallita']));
}
$id_booking_class = isset($_POST['id']) ? intval($_POST['id']) : 0;
$class_id = isset($_POST['class_id']) ? intval($_POST['class_id']) : 0;
$new_date = isset($_POST['new_date']) ? $_POST['new_date'] : '';
if ($id_booking_class <= 0 || $class_id <= 0 || empty($new_date)) {
die(json_encode(['success' => false, 'message' => 'Dati non validi']));
}
// Ottieni i dettagli della classe attuale
$sql = "SELECT bookingstart, servicename, day, time FROM bookingclass WHERE idbookingclass = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'i', $id_booking_class);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$current_class = mysqli_fetch_assoc($result);
if (!$current_class) {
die(json_encode(['success' => false, 'message' => 'Lezione non trovata']));
}
// Ottieni i dettagli della nuova classe
$sql = "SELECT servicename, day, time FROM classes WHERE id = ?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'i', $class_id);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
$new_class = mysqli_fetch_assoc($result);
if (!$new_class) {
die(json_encode(['success' => false, 'message' => 'Classe non trovata']));
}
// Aggiorna la lezione
$sql = "UPDATE bookingclass
SET bookingstart = ?, prevbookingstart = ?, servicename = ?, day = ?, time = ?
WHERE idbookingclass = ?";
$stmt = mysqli_prepare($conn, $sql);
$new_bookingstart = $new_date . ' ' . $new_class['time'] . ':00';
mysqli_stmt_bind_param($stmt, 'sssssi', $new_bookingstart, $current_class['bookingstart'], $new_class['servicename'], $new_class['day'], $new_class['time'], $id_booking_class);
if (mysqli_stmt_execute($stmt)) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Errore durante l\'aggiornamento della lezione']);
}
mysqli_stmt_close($stmt);
mysqli_close($conn);

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

View File

@ -0,0 +1,36 @@
YogaSoul Autoresponder — Istruzioni rapide
COS'È
- Crea BOZZE di risposta (non invia) per le ultime X email NON LETTE in INBOX.
- Le bozze vengono salvate nella cartella Drafts/Bozze dellaccount IMAP, in thread (In-Reply-To/References) con citazione del messaggio originale.
FILE NELLA CARTELLA
- yogasoul_autoresponder.exe (oppure: email_autoresponder_yogasoul.py se lo usi con Python)
- config.json (credenziali e impostazioni)
- prompt_template.txt (testo/tono della risposta: modificabile liberamente)
- yogasoul_knowledge_base.json (dati corsi, link, ecc.)
COME SI USA (Windows, senza Python)
1) Apri la cartella “YogaSoul Autoresponder”.
2) Modifica `config.json` con:
- imap_server, email_address, email_password
- openai_api_key
- max_to_process (es. 5), throttle_seconds (es. 1.5)
3) (Opz.) Modifica `prompt_template.txt` e/o `yogasoul_knowledge_base.json`.
4) Doppio click su `yogasoul_autoresponder.exe` (oppure su `run_autoresponder.bat` se fornito).
5) Controlla in Posta le BOZZE: dovresti vedere una bozza per ogni email non letta (fino a max_to_process).
IMPOSTAZIONI UTILI (config.json)
- "max_to_process": quante email non lette processare (consigliato 5).
- "throttle_seconds": pausa tra le richieste (consigliato 12s).
- "mark_as_seen": true per segnare come lette dopo la bozza (false per lasciarle non lette).
- "preferred_draft_folder": cartella locale alternativa se il server non ha Drafts.
TROUBLESHOOTING
- Nessuna bozza: verifica credenziali IMAP/API in config.json; verifica che ci siano email UNSEEN.
- Bozze “schiacciate”: il programma converte in HTML con <p>/<br>; se serve, modifica il prompt.
- Rate limit/blocchi: alza "throttle_seconds" (es. 1.52.0) e tieni "max_to_process" basso (5).
- Cartella bozze: il programma cerca \Drafts; in fallback crea INBOX.BozzaRisposte.
SICUREZZA
- `config.json` contiene password in chiaro: conserva la cartella su un PC/utente fidato.

View File

@ -0,0 +1,11 @@
{
"imap_server": "mail.yogasoul.it",
"email_address": "info@yogasoul.it",
"email_password": "!Testolina88",
"openai_api_key": "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",
"openai_model": "gpt-3.5-turbo",
"preferred_draft_folder": "BozzaRisposte",
"mark_as_seen": true,
"throttle_seconds": 2,
"max_to_process": 5
}

View File

@ -13,20 +13,43 @@ from email.utils import parseaddr
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.header import decode_header, make_header from email.header import decode_header, make_header
from datetime import datetime from datetime import datetime
from pathlib import Path
import sys
# ========================== # ==========================
# CONFIG (compila qui) # CONFIG da config.json (accanto allo script/EXE)
# ========================== # ==========================
IMAP_SERVER = "mail.yogasoul.it" def app_dir() -> Path:
EMAIL_ADDRESS = "info@yogasoul.it" # se "frozen" (PyInstaller), usa la cartella dell'eseguibile
EMAIL_PASSWORD = "!Testolina88" # <— METTI LA PASSWORD QUI return Path(sys.executable).parent if getattr(sys, "frozen", False) else Path(__file__).resolve().parent
OPENAI_API_KEY = "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" # <— METTI LA OPENAI KEY QUI
KB_PATH = "yogasoul_knowledge_base.json" CONFIG_PATH = app_dir() / "config.json"
OPENAI_MODEL = "gpt-3.5-turbo"
PREFERRED_DRAFT_FOLDER = "BozzaRisposte" # se non trova Drafts, crea INBOX.<questa> def load_config():
MARK_AS_SEEN = True # False per lasciare le email non lette try:
THROTTLE_SECONDS = 0 # ritardo tra email (0 = nessuno) with open(CONFIG_PATH, "r", encoding="utf-8") as f:
MAX_TO_PROCESS = 5 # ⬅️ NUOVO: processa al massimo N email UNSEEN (ultime) cfg = json.load(f)
print(f"[CONFIG] Caricato: {CONFIG_PATH}")
return cfg
except Exception as e:
print(f"[ERRORE] Impossibile leggere {CONFIG_PATH}: {e}")
return {}
CFG = load_config()
# valori letti dal config (con default sensati)
IMAP_SERVER = CFG.get("imap_server", "mail.yogasoul.it")
EMAIL_ADDRESS = CFG.get("email_address", "")
EMAIL_PASSWORD = CFG.get("email_password", "")
OPENAI_API_KEY = CFG.get("openai_api_key", "")
OPENAI_MODEL = CFG.get("openai_model", "gpt-3.5-turbo")
PREFERRED_DRAFT_FOLDER = CFG.get("preferred_draft_folder", "BozzaRisposte")
MARK_AS_SEEN = bool(CFG.get("mark_as_seen", True))
THROTTLE_SECONDS = float(CFG.get("throttle_seconds", 0) or 0)
MAX_TO_PROCESS = int(CFG.get("max_to_process", 5) or 5)
# Percorsi esterni (accanto all'eseguibile)
KB_PATH = str(app_dir() / CFG.get("kb_path", "yogasoul_knowledge_base.json"))
PROMPT_PATH = str(app_dir() / CFG.get("prompt_path", "prompt_template.txt"))
# ========================== # ==========================
# UTILS # UTILS
@ -72,7 +95,6 @@ def _ensure_html_blocks(s):
body = "<p>{}</p>".format(html_lib.escape(s).replace("\n", "<br>")) body = "<p>{}</p>".format(html_lib.escape(s).replace("\n", "<br>"))
return "<!doctype html><html><body>{}</body></html>".format(body) return "<!doctype html><html><body>{}</body></html>".format(body)
def _make_quoted_original(body_text): def _make_quoted_original(body_text):
"""Crea il blocco citato del messaggio originale, safe-escaped.""" """Crea il blocco citato del messaggio originale, safe-escaped."""
if not body_text: if not body_text:
@ -84,6 +106,14 @@ def _make_quoted_original(body_text):
f"<blockquote style='margin:0 0 0 1em; padding-left:1em; border-left:3px solid #ddd'>{escaped}</blockquote>" f"<blockquote style='margin:0 0 0 1em; padding-left:1em; border-left:3px solid #ddd'>{escaped}</blockquote>"
) )
def _inject_before_body_end(html_src: str, addition: str) -> str:
"""Inserisce 'addition' prima di </body> in modo case-insensitive; se manca </body>, appende."""
m = re.search(r'</\s*body\s*>', html_src, flags=re.I)
if not m:
return html_src + addition
start = m.start()
return html_src[:start] + addition + html_src[start:]
def load_knowledge_base(path=KB_PATH): def load_knowledge_base(path=KB_PATH):
if not os.path.isfile(path): if not os.path.isfile(path):
print(f"[ATTENZIONE] KB non trovata: {path}. Proseguo senza.") print(f"[ATTENZIONE] KB non trovata: {path}. Proseguo senza.")
@ -98,20 +128,39 @@ def load_knowledge_base(path=KB_PATH):
def assert_config(): def assert_config():
problems = [] problems = []
if not isinstance(IMAP_SERVER, str) or not IMAP_SERVER.strip(): if not isinstance(IMAP_SERVER, str) or not IMAP_SERVER.strip():
problems.append("IMAP_SERVER mancante") problems.append("imap_server mancante in config.json")
if not isinstance(EMAIL_ADDRESS, str) or not EMAIL_ADDRESS.strip(): if not isinstance(EMAIL_ADDRESS, str) or not EMAIL_ADDRESS.strip():
problems.append("EMAIL_ADDRESS mancante") problems.append("email_address mancante in config.json")
if not isinstance(EMAIL_PASSWORD, str) or not EMAIL_PASSWORD.strip() or EMAIL_PASSWORD == "INSERISCI_LA_PASSWORD": if not isinstance(EMAIL_PASSWORD, str) or not EMAIL_PASSWORD.strip():
problems.append("EMAIL_PASSWORD non impostata") problems.append("email_password mancante in config.json")
if not isinstance(OPENAI_API_KEY, str) or not OPENAI_API_KEY.strip() or OPENAI_API_KEY == "INSERISCI_OPENAI_API_KEY": if not isinstance(OPENAI_API_KEY, str) or not OPENAI_API_KEY.strip():
problems.append("OPENAI_API_KEY non impostata") problems.append("openai_api_key mancante in config.json")
if problems: if problems:
print("[CONFIG] Correggi questi parametri prima di proseguire:") print("[CONFIG] Correggi config.json:")
for p in problems: for p in problems:
print(" -", p) print(" -", p)
return False return False
return True return True
# ==========================
# Prompt esterno
# ==========================
def load_prompt(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as f:
print(f"[PROMPT] Caricato: {path}")
return f.read()
except Exception as e:
print(f"[ATTENZIONE] Prompt non trovato o illeggibile: {path} ({e})")
return ""
def render_prompt(template: str, **vars_) -> str:
# Sostituzione semplice stile {{nome}}
out = template
for k, v in vars_.items():
out = out.replace(f"{{{{{k}}}}}", str(v))
return out
# ========================== # ==========================
# IMAP helpers robusti # IMAP helpers robusti
# ========================== # ==========================
@ -229,25 +278,33 @@ def generate_response(email_info, kb):
f"Corpo: {email_info.get('body_text','')}" f"Corpo: {email_info.get('body_text','')}"
) )
prompt = f""" # Prompt da file esterno (con fallback interno)
Sei Aurora, fondatrice di YogaSoul (www.yogasoul.it), stile zen e informale, diretto. template = load_prompt(PROMPT_PATH)
Rispondi in italiano, amichevole e rilassato, con emoticon yoga (🌿, 🧘, 😊) senza esagerare. if not template:
Saluta con "Ciao {nickname}, bello sentirti!" e firma con "Namaste, Aurora - YogaSoul". template = (
Usa la knowledge base per corsi, orari, prezzi, benefici, insegnanti. "Sei Aurora, fondatrice di YogaSoul (www.yogasoul.it), stile zen e informale, diretto.\n"
Includi sempre il link_prenotazione specifico come <a href='link'>Iscriviti qui</a> per prenotazioni, "Rispondi in italiano, amichevole e rilassato, con emoticon yoga (🌿, 🧘‍♀️, 😊) senza esagerare.\n"
e il calendario <a href='https://yogasoul.it/wp-content/uploads/2025/08/Calendario-settembre-2025-2.jpg'>qui</a>. "Saluta con \"Ciao {{nickname}}, bello sentirti!\" e firma con \"Namaste, Aurora - YogaSoul\".\n"
Se non sai, scrivi: "Contattami per dettagli! 🧘‍♀️". "Usa la knowledge base per corsi, orari, prezzi, benefici, insegnanti.\n"
"Includi sempre il link_prenotazione specifico come <a href='link'>Iscriviti qui</a> per prenotazioni,\n"
"e il calendario <a href='https://yogasoul.it/wp-content/uploads/2025/08/Calendario-settembre-2025-2.jpg'>qui</a>.\n"
"Se non sai, scrivi: \"Contattami per dettagli! 🧘‍♀️\".\n\n"
"{{policy}}\n\n"
"Knowledge Base:\n"
"{{kb_json}}\n\n"
"Email ricevuta:\n"
"{{email_text}}\n\n"
"Scrivi la risposta in HTML pulito (usa <p>, <ul>/<li> se utile; niente CSS superfluo).\n"
)
{policy} prompt = render_prompt(
template,
nickname=nickname,
policy=policy,
kb_json=kb_json,
email_text=email_text,
)
Knowledge Base:
{kb_json}
Email ricevuta:
{email_text}
Scrivi la risposta in HTML pulito (usa <p>, <ul>/<li> se utile; niente CSS superfluo).
"""
resp = client.chat.completions.create( resp = client.chat.completions.create(
model=OPENAI_MODEL, model=OPENAI_MODEL,
messages=[{"role": "user", "content": prompt}], messages=[{"role": "user", "content": prompt}],
@ -283,7 +340,7 @@ def fetch_all_unseen(mail, limit=None):
return results return results
if limit and limit > 0: if limit and limit > 0:
ids = ids[-limit:] # ⬅️ prendi SOLO le ultime N non lette ids = ids[-limit:] # prendi SOLO le ultime N non lette
for seq_id in ids: for seq_id in ids:
typ, msg_data = mail.fetch(seq_id, "(RFC822)") typ, msg_data = mail.fetch(seq_id, "(RFC822)")
@ -335,9 +392,10 @@ def fetch_all_unseen(mail, limit=None):
def prepare_reply_mime(email_info, response_html): def prepare_reply_mime(email_info, response_html):
"""Costruisce il MIME HTML della risposta con quote e thread headers.""" """Costruisce il MIME HTML della risposta con quote e thread headers."""
reply_body = (response_html.replace("</body>", _make_quoted_original(email_info.get("body_text","")) + "</body>") reply_body = _inject_before_body_end(
if "</body>" in response_html.lower() response_html,
else response_html + _make_quoted_original(email_info.get("body_text",""))) _make_quoted_original(email_info.get("body_text",""))
)
msg = MIMEText(reply_body, "html", "utf-8") msg = MIMEText(reply_body, "html", "utf-8")
subj = email_info.get("subject") or "[Risposta automatica] - YogaSoul" subj = email_info.get("subject") or "[Risposta automatica] - YogaSoul"
@ -388,9 +446,23 @@ def append_draft(mail, folder, msg):
# MAIN # MAIN
# ========================== # ==========================
def main(): def main():
if not assert_config(): # Controllo config di base
problems = []
if not isinstance(IMAP_SERVER, str) or not IMAP_SERVER.strip():
problems.append("imap_server mancante in config.json")
if not isinstance(EMAIL_ADDRESS, str) or not EMAIL_ADDRESS.strip():
problems.append("email_address mancante in config.json")
if not isinstance(EMAIL_PASSWORD, str) or not EMAIL_PASSWORD.strip():
problems.append("email_password mancante in config.json")
if not isinstance(OPENAI_API_KEY, str) or not OPENAI_API_KEY.strip():
problems.append("openai_api_key mancante in config.json")
if problems:
print("[CONFIG] Correggi config.json:")
for p in problems:
print(" -", p)
return return
# Login IMAP
try: try:
mail = imaplib.IMAP4_SSL(IMAP_SERVER) mail = imaplib.IMAP4_SSL(IMAP_SERVER)
mail.login(EMAIL_ADDRESS, EMAIL_PASSWORD) mail.login(EMAIL_ADDRESS, EMAIL_PASSWORD)
@ -403,7 +475,7 @@ def main():
drafts_folder, delim = ensure_drafts_folder(mail) drafts_folder, delim = ensure_drafts_folder(mail)
print(f"[DEBUG] drafts_folder='{drafts_folder}' delim='{delim}'") print(f"[DEBUG] drafts_folder='{drafts_folder}' delim='{delim}'")
emails = fetch_all_unseen(mail, limit=MAX_TO_PROCESS) # ⬅️ usa il limite emails = fetch_all_unseen(mail, limit=MAX_TO_PROCESS)
if not emails: if not emails:
print("Nessuna nuova email da processare.") print("Nessuna nuova email da processare.")
mail.logout() mail.logout()

View File

@ -0,0 +1,17 @@
Sei Aurora, fondatrice di YogaSoul (www.yogasoul.it), stile zen e informale, diretto.
Rispondi in italiano, amichevole e rilassato, con emoticon yoga (🌿, 🧘‍♀️, 😊) senza esagerare.
Saluta con "Ciao {{nickname}}, bello sentirti!" e firma con "Namaste, Aurora - YogaSoul".
Usa la knowledge base per corsi, orari, prezzi, benefici, insegnanti.
Includi sempre il link_prenotazione specifico come <a href='link'>Iscriviti qui</a> per prenotazioni,
e il calendario <a href='https://yogasoul.it/wp-content/uploads/2025/08/Calendario-settembre-2025-2.jpg'>qui</a>.
Se non sai, scrivi: "Contattami per dettagli! 🧘‍♀️".
{{policy}}
Knowledge Base:
{{kb_json}}
Email ricevuta:
{{email_text}}
Scrivi la risposta in HTML pulito (usa <p>, <ul>/<li> se utile; niente CSS superfluo).