#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import re
import time
import json
import imaplib
import email
import traceback
import html as html_lib
from email.utils import parseaddr
from email.mime.text import MIMEText
from email.header import decode_header, make_header
from datetime import datetime
# ==========================
# CONFIG (compila qui)
# ==========================
IMAP_SERVER = "mail.yogasoul.it"
EMAIL_ADDRESS = "info@yogasoul.it"
EMAIL_PASSWORD = "!Testolina88" # <— METTI LA PASSWORD QUI
OPENAI_API_KEY = "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" # <— METTI LA OPENAI KEY QUI
KB_PATH = "yogasoul_knowledge_base.json"
OPENAI_MODEL = "gpt-3.5-turbo"
PREFERRED_DRAFT_FOLDER = "BozzaRisposte" # se non trova Drafts, crea INBOX.
MARK_AS_SEEN = True # False per lasciare le email non lette
THROTTLE_SECONDS = 0 # ritardo tra email (0 = nessuno)
MAX_TO_PROCESS = 5 # ⬅️ NUOVO: processa al massimo N email UNSEEN (ultime)
# ==========================
# UTILS
# ==========================
def _decode_mime_words(s):
if not s:
return ""
try:
return str(make_header(decode_header(s)))
except Exception:
parts = decode_header(s)
out = []
for text, enc in parts:
if isinstance(text, bytes):
out.append(text.decode(enc or "utf-8", errors="ignore"))
else:
out.append(text or "")
return "".join(out)
def _html_to_text(html):
try:
text = re.sub(r"(?is)<(script|style).*?>.*?\1>", "", html or "")
text = re.sub(r"(?s)
", "\n", text)
text = re.sub(r"(?s)
", "\n\n", text)
text = re.sub(r"(?s)<.*?>", "", text)
return text.strip()
except Exception:
return html or ""
def _ensure_html_blocks(s):
"""Se il modello restituisce testo piatto, convertilo in HTML semplice e leggibile."""
s = (s or "").strip()
if " 1:
body = "".join(f"{html_lib.escape(p).replace('\n','
')}
" for p in parts)
else:
body = "" + html_lib.escape(s).replace("\n", "
") + "
"
return f"{body}"
def _make_quoted_original(body_text):
"""Crea il blocco citato del messaggio originale, safe-escaped."""
if not body_text:
return ""
escaped = html_lib.escape(body_text).replace("\n", "
")
return (
"
"
"— Messaggio originale —
"
f"{escaped}
"
)
def load_knowledge_base(path=KB_PATH):
if not os.path.isfile(path):
print(f"[ATTENZIONE] KB non trovata: {path}. Proseguo senza.")
return {}
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
print(f"[ERRORE] Lettura KB: {e}. Proseguo senza.")
return {}
def assert_config():
problems = []
if not isinstance(IMAP_SERVER, str) or not IMAP_SERVER.strip():
problems.append("IMAP_SERVER mancante")
if not isinstance(EMAIL_ADDRESS, str) or not EMAIL_ADDRESS.strip():
problems.append("EMAIL_ADDRESS mancante")
if not isinstance(EMAIL_PASSWORD, str) or not EMAIL_PASSWORD.strip() or EMAIL_PASSWORD == "INSERISCI_LA_PASSWORD":
problems.append("EMAIL_PASSWORD non impostata")
if not isinstance(OPENAI_API_KEY, str) or not OPENAI_API_KEY.strip() or OPENAI_API_KEY == "INSERISCI_OPENAI_API_KEY":
problems.append("OPENAI_API_KEY non impostata")
if problems:
print("[CONFIG] Correggi questi parametri prima di proseguire:")
for p in problems:
print(" -", p)
return False
return True
# ==========================
# IMAP helpers robusti
# ==========================
LIST_RE = re.compile(r'^\s*\((?P[^)]*)\)\s+"(?P[^"]+)"\s+(?P.+?)\s*$')
def _parse_list_line(raw: bytes):
s = raw.decode(errors="ignore")
m = LIST_RE.match(s)
if not m:
parts = s.strip().split()
name = parts[-1] if parts else ""
name = name.strip('"')
return ([], ".", name)
flags_str = m.group("flags") or ""
delim = m.group("delim") or "."
name = m.group("name").strip()
if name.startswith('"') and name.endswith('"'):
name = name[1:-1]
flags = [f.strip() for f in flags_str.split() if f.strip()]
return (flags, delim, name)
def _list_mailboxes(mail):
try:
typ, boxes = mail.list()
if typ != "OK":
return []
return [_parse_list_line(raw) for raw in (boxes or [])]
except Exception:
return []
def _find_drafts_mailbox(mail):
boxes = _list_mailboxes(mail)
if not boxes:
return (None, ".")
delim_guess = boxes[0][1] if boxes[0][1] else "."
# special-use \Drafts
for flags, delim, name in boxes:
if any("\\Drafts" in f or "\\drafts" in f for f in flags):
return (name, delim or delim_guess)
# nomi comuni
wanted = ("drafts", "bozze", "bozza", "draft")
for flags, delim, name in boxes:
low = name.lower()
if any(w in low.split(delim or ".")[-1] for w in wanted):
return (name, delim or delim_guess)
return (None, delim_guess)
def _select_or_create(mail, name, delim):
typ, _ = mail.select(name, readonly=False)
if typ == "OK":
return name
candidate = f"INBOX{delim}{name}"
typ, _ = mail.select(candidate, readonly=False)
if typ == "OK":
return candidate
mail.create(candidate)
typ, _ = mail.select(candidate, readonly=False)
if typ == "OK":
return candidate
raise RuntimeError(f"Impossibile selezionare o creare la casella '{name}' (delim='{delim}')")
# ==========================
# INTENT DETECTION (semplice)
# ==========================
SCHEDULE_WORDS = [
"orario", "orari", "quando", "che ore", "a che ora", "giorni", "mercoledì", "martedì",
"lezione di", "inizia", "finisce", "durata"
]
BOOKING_WORDS = [
"prenota", "prenotazione", "prenotare", "iscriversi", "iscrizione", "link", "come fare", "dove prenoto"
]
INFO_WORDS = [
"informazioni", "info", "cos'è", "che cos", "benefici", "a chi è adatto", "livello", "programma",
"insegnante", "maestro", "costi", "prezzo", "quanto costa", "materiale", "cosa portare"
]
def classify_intent(subject, body):
s = f"{subject or ''} {body or ''}".lower()
has_info = any(w in s for w in INFO_WORDS)
has_sched = any(w in s for w in SCHEDULE_WORDS)
has_book = any(w in s for w in BOOKING_WORDS)
if has_info:
return "extended"
if (has_sched or has_book) and not has_info:
return "brief"
return "brief"
# ==========================
# OPENAI: GENERAZIONE BOZZA
# ==========================
def generate_response(email_info, kb):
try:
from openai import OpenAI
client = OpenAI(api_key=OPENAI_API_KEY)
nickname = "amico"
if email_info.get("sender_name"):
nickname = email_info["sender_name"].split()[0]
elif email_info.get("sender_email"):
nickname = email_info["sender_email"].split("@")[0].split(".")[0] or "amico"
kb_json = json.dumps(kb, ensure_ascii=False, indent=2)
intent = classify_intent(email_info.get("subject",""), email_info.get("body_text",""))
if intent == "brief":
policy = ("Se il messaggio chiede solo orari e/o come prenotare, rispondi BREVE: "
"indica orari precisi e inserisci SOLO il link prenotazione. Non aggiungere benefici o descrizioni.")
else:
policy = ("Se il messaggio chiede informazioni sul corso, rispondi ESTESO: "
"includi orari, benefici principali, a chi è adatto, eventuale insegnante, e il link prenotazione.")
email_text = (
f"Soggetto: {email_info.get('subject','')}\n"
f"Mittente: {email_info.get('sender_email','')}\n"
f"Corpo: {email_info.get('body_text','')}"
)
prompt = f"""
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 Iscriviti qui per prenotazioni,
e il calendario qui.
Se non sai, scrivi: "Contattami per dettagli! 🧘♀️".
{policy}
Knowledge Base:
{kb_json}
Email ricevuta:
{email_text}
Scrivi la risposta in HTML pulito (usa ,