first commit
This commit is contained in:
commit
e6c52820cd
227 changed files with 16156 additions and 0 deletions
353
modules/email/email_manager.py
Normal file
353
modules/email/email_manager.py
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
from typing import List, Dict, Any, Union
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from datetime import datetime
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from urllib.parse import quote_plus
|
||||
from uuid import uuid4
|
||||
from core.data import Data
|
||||
|
||||
class EmailTemplate:
|
||||
"""Classe gérant les templates d'emails"""
|
||||
|
||||
def __init__(self, template_folder="Data/email_templates"):
|
||||
self.template_folder = template_folder
|
||||
|
||||
# Créer le dossier de templates s'il n'existe pas
|
||||
if not os.path.exists(self.template_folder):
|
||||
os.makedirs(self.template_folder)
|
||||
|
||||
def get_all_templates(self):
|
||||
"""Retourne tous les templates disponibles"""
|
||||
templates = []
|
||||
if os.path.exists(self.template_folder):
|
||||
for filename in os.listdir(self.template_folder):
|
||||
if filename.endswith('.json'):
|
||||
template_path = os.path.join(self.template_folder, filename)
|
||||
try:
|
||||
data_manager = Data(template_path)
|
||||
template_data = data_manager.load_data()
|
||||
templates.append(template_data)
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du chargement du template {filename}: {e}")
|
||||
return templates
|
||||
|
||||
def get_template_by_id(self, template_id):
|
||||
"""Récupère un template par son ID"""
|
||||
template_path = os.path.join(self.template_folder, f"{template_id}.json")
|
||||
if os.path.exists(template_path):
|
||||
data_manager = Data(template_path)
|
||||
return data_manager.load_data()
|
||||
return None
|
||||
|
||||
def save_template(self, template_data):
|
||||
"""Sauvegarde un template d'email"""
|
||||
template_id = template_data.get('id')
|
||||
if not template_id:
|
||||
# Générer un ID s'il n'existe pas
|
||||
import uuid
|
||||
template_id = f"tpl_{uuid.uuid4().hex[:8]}"
|
||||
template_data['id'] = template_id
|
||||
|
||||
template_path = os.path.join(self.template_folder, f"{template_id}.json")
|
||||
data_manager = Data(template_path)
|
||||
data_manager.save_data(template_data)
|
||||
return template_data
|
||||
|
||||
def delete_template(self, template_id):
|
||||
"""Supprime un template d'email"""
|
||||
template_path = os.path.join(self.template_folder, f"{template_id}.json")
|
||||
if os.path.exists(template_path):
|
||||
os.remove(template_path)
|
||||
return True
|
||||
return False
|
||||
|
||||
def render_template(self, template_id, context=None):
|
||||
"""Rend un template avec les variables spécifiées dans le contexte"""
|
||||
template = self.get_template_by_id(template_id)
|
||||
if not template:
|
||||
return None
|
||||
|
||||
subject = template.get('subject', '')
|
||||
content = template.get('content', '')
|
||||
|
||||
# Remplacer les variables dans le sujet et le contenu
|
||||
if context:
|
||||
for key, value in context.items():
|
||||
placeholder = f"{{{{{key}}}}}"
|
||||
subject = subject.replace(placeholder, str(value))
|
||||
content = content.replace(placeholder, str(value))
|
||||
|
||||
return {
|
||||
"subject": subject,
|
||||
"content": content
|
||||
}
|
||||
|
||||
|
||||
class EmailSender:
|
||||
"""Classe gérant l'envoi d'emails"""
|
||||
|
||||
def __init__(self, config_file="config/email_config.json"):
|
||||
# Ensure config_file is an absolute path
|
||||
if not os.path.isabs(config_file):
|
||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
self.config_file = os.path.join(base_dir, config_file)
|
||||
else:
|
||||
self.config_file = config_file
|
||||
self.config = self._load_config()
|
||||
self.template_manager = EmailTemplate()
|
||||
|
||||
def _load_config(self):
|
||||
"""Charge la configuration email depuis le fichier de configuration"""
|
||||
config_dir = os.path.dirname(self.config_file)
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir)
|
||||
|
||||
print(f"Loading email config from: {self.config_file}")
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
print(f"Loaded email config: {config}")
|
||||
return config
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du chargement de la configuration email: {e}")
|
||||
|
||||
# Configuration par défaut
|
||||
default_config = {
|
||||
"smtp_server": "smtp.gmail.com",
|
||||
"smtp_port": 587,
|
||||
"username": "",
|
||||
"password": "",
|
||||
"sender_name": "Suite Consultance",
|
||||
"sender_email": ""
|
||||
}
|
||||
print(f"Using default email config: {default_config}")
|
||||
return default_config
|
||||
|
||||
def save_config(self, config):
|
||||
"""Sauvegarde la configuration email"""
|
||||
config_dir = os.path.dirname(self.config_file)
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir)
|
||||
|
||||
with open(self.config_file, 'w') as f:
|
||||
json.dump(config, f, indent=4)
|
||||
|
||||
self.config = config
|
||||
return True
|
||||
|
||||
def send_email(self, to_email, subject, body, cc=None, bcc=None):
|
||||
"""Envoie un email à un destinataire"""
|
||||
if not self.config.get('username') or not self.config.get('password'):
|
||||
raise ValueError("La configuration email n'est pas complète")
|
||||
|
||||
message = MIMEMultipart()
|
||||
message["From"] = f"{self.config.get('sender_name')} <{self.config.get('sender_email')}>"
|
||||
message["To"] = to_email
|
||||
message["Subject"] = subject
|
||||
|
||||
if cc:
|
||||
message["Cc"] = ", ".join(cc) if isinstance(cc, list) else cc
|
||||
if bcc:
|
||||
message["Bcc"] = ", ".join(bcc) if isinstance(bcc, list) else bcc
|
||||
|
||||
message.attach(MIMEText(body, "html"))
|
||||
|
||||
try:
|
||||
server = smtplib.SMTP(self.config.get('smtp_server'), self.config.get('smtp_port'))
|
||||
server.starttls()
|
||||
server.login(self.config.get('username'), self.config.get('password'))
|
||||
|
||||
recipients = [to_email]
|
||||
if cc:
|
||||
recipients.extend(cc if isinstance(cc, list) else [cc])
|
||||
if bcc:
|
||||
recipients.extend(bcc if isinstance(bcc, list) else [bcc])
|
||||
|
||||
server.sendmail(self.config.get('sender_email'), recipients, message.as_string())
|
||||
server.quit()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"to": to_email,
|
||||
"subject": subject
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
def send_templated_email(self, to_email, template_id, context=None, cc=None, bcc=None):
|
||||
"""Envoie un email basé sur un template à un destinataire"""
|
||||
rendered = self.template_manager.render_template(template_id, context)
|
||||
if not rendered:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Template not found",
|
||||
"timestamp": datetime.now().isoformat()
|
||||
}
|
||||
|
||||
return self.send_email(to_email, rendered['subject'], rendered['content'], cc, bcc)
|
||||
|
||||
def send_bulk_email(self, emails, subject, body, cc=None, bcc=None):
|
||||
"""Envoie le même email à plusieurs destinataires"""
|
||||
results = []
|
||||
for email in emails:
|
||||
result = self.send_email(email, subject, body, cc, bcc)
|
||||
results.append({
|
||||
"email": email,
|
||||
**result
|
||||
})
|
||||
return results
|
||||
|
||||
def send_bulk_templated_email(self, recipients, template_id, cc=None, bcc=None):
|
||||
"""
|
||||
Envoie un email basé sur un template à plusieurs destinataires
|
||||
recipients: liste de dictionnaires contenant l'email du destinataire et le contexte
|
||||
[{
|
||||
"email": "example@example.com",
|
||||
"context": {"name": "John Doe", "company": "ACME Inc."}
|
||||
}]
|
||||
"""
|
||||
results = []
|
||||
for recipient in recipients:
|
||||
email = recipient.get('email')
|
||||
context = recipient.get('context', {})
|
||||
result = self.send_templated_email(email, template_id, context, cc, bcc)
|
||||
results.append({
|
||||
"email": email,
|
||||
**result
|
||||
})
|
||||
return results
|
||||
|
||||
# ---------- Tracking helpers ----------
|
||||
def _embed_tracking(self, html_body: str, tracking_id: str, prospect_id: str) -> str:
|
||||
"""
|
||||
Ajoute un pixel d'ouverture et réécrit les liens pour le click tracking.
|
||||
Utilise APP_BASE_URL si définie, sinon génère des liens relatifs.
|
||||
"""
|
||||
base = (os.environ.get("APP_BASE_URL") or "").rstrip("/")
|
||||
prefix = f"{base}/tasks/t" # routes de tracking montées sur le blueprint 'tasks'
|
||||
# Pixel d'ouverture (1x1 PNG)
|
||||
pixel = f'<img src="{prefix}/o/{tracking_id}.png?pid={quote_plus(prospect_id)}" alt="" width="1" height="1" style="display:none;" />'
|
||||
body = html_body or ""
|
||||
# Injection du pixel avant la fermeture de body si possible
|
||||
if "</body>" in body.lower():
|
||||
# trouver la vraie balise en conservant la casse
|
||||
idx = body.lower().rfind("</body>")
|
||||
body = body[:idx] + pixel + body[idx:]
|
||||
else:
|
||||
body = body + pixel
|
||||
|
||||
# Réécriture des liens <a href="...">
|
||||
def _rewrite(match):
|
||||
url = match.group(1)
|
||||
# ignore si déjà tracké
|
||||
if "/tasks/t/c/" in url:
|
||||
return f'href="{url}"'
|
||||
tracked = f'{prefix}/c/{tracking_id}?u={quote_plus(url)}'
|
||||
return f'href="{tracked}"'
|
||||
|
||||
body = re.sub(r'href="([^"]+)"', _rewrite, body)
|
||||
return body
|
||||
|
||||
def send_tracked_email(self, to_email: str, subject: str, body: str, prospect_id: str, template_id: str = None, cc=None, bcc=None) -> Dict[str, Any]:
|
||||
"""
|
||||
Envoie un email avec tracking (open/click).
|
||||
Crée un enregistrement de tracking et insère un pixel + réécriture des liens.
|
||||
"""
|
||||
tracking_id = f"trk_{uuid4().hex[:16]}"
|
||||
# Créer l'enregistrement de tracking
|
||||
try:
|
||||
from modules.tracking.store import TrackingStore
|
||||
store = TrackingStore()
|
||||
store.create_record(tracking_id, {
|
||||
"prospect_id": prospect_id,
|
||||
"to": to_email,
|
||||
"subject": subject,
|
||||
"template_id": template_id,
|
||||
"opens": 0,
|
||||
"clicks": 0,
|
||||
})
|
||||
except Exception:
|
||||
# même si le tracking store échoue, on tente d'envoyer l'email
|
||||
pass
|
||||
|
||||
tracked_body = self._embed_tracking(body, tracking_id, prospect_id)
|
||||
result = self.send_email(to_email, subject, tracked_body, cc, bcc)
|
||||
result["tracking_id"] = tracking_id
|
||||
return result
|
||||
|
||||
|
||||
class EmailHistory:
|
||||
"""Classe gérant l'historique des emails envoyés"""
|
||||
|
||||
def __init__(self, history_folder="Data/email_history"):
|
||||
self.history_folder = history_folder
|
||||
|
||||
# Créer le dossier d'historique s'il n'existe pas
|
||||
if not os.path.exists(self.history_folder):
|
||||
os.makedirs(self.history_folder)
|
||||
|
||||
def add_email_record(self, prospect_id, email_data):
|
||||
"""Ajoute un email à l'historique d'un prospect"""
|
||||
history_file = os.path.join(self.history_folder, f"{prospect_id}.json")
|
||||
|
||||
# Charger l'historique existant
|
||||
history = []
|
||||
if os.path.exists(history_file):
|
||||
try:
|
||||
with open(history_file, 'r') as f:
|
||||
history = json.load(f)
|
||||
except:
|
||||
history = []
|
||||
|
||||
# Ajouter le nouvel email à l'historique
|
||||
history.append({
|
||||
**email_data,
|
||||
"timestamp": datetime.now().isoformat()
|
||||
})
|
||||
|
||||
# Sauvegarder l'historique
|
||||
with open(history_file, 'w') as f:
|
||||
json.dump(history, f, indent=4)
|
||||
|
||||
return True
|
||||
|
||||
def get_prospect_email_history(self, prospect_id):
|
||||
"""Récupère l'historique des emails pour un prospect"""
|
||||
history_file = os.path.join(self.history_folder, f"{prospect_id}.json")
|
||||
|
||||
if os.path.exists(history_file):
|
||||
try:
|
||||
with open(history_file, 'r') as f:
|
||||
return json.load(f)
|
||||
except:
|
||||
return []
|
||||
|
||||
return []
|
||||
|
||||
def get_all_email_history(self):
|
||||
"""Récupère l'historique de tous les emails envoyés"""
|
||||
all_history = {}
|
||||
|
||||
if os.path.exists(self.history_folder):
|
||||
for filename in os.listdir(self.history_folder):
|
||||
if filename.endswith('.json'):
|
||||
prospect_id = filename.split('.')[0]
|
||||
history_file = os.path.join(self.history_folder, filename)
|
||||
|
||||
try:
|
||||
with open(history_file, 'r') as f:
|
||||
all_history[prospect_id] = json.load(f)
|
||||
except:
|
||||
all_history[prospect_id] = []
|
||||
|
||||
return all_history
|
||||
Loading…
Add table
Add a link
Reference in a new issue