import win32com.client import hashlib import pythoncom import re import platform import os import math from datetime import datetime from urllib.parse import urlparse from PyQt5.QtWebEngineWidgets import QWebEngineSettings, QWebEngineProfile from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QListWidgetItem, QLabel, QListWidget, QLineEdit, QTreeWidget, QComboBox, QPushButton, QProgressBar, QFrame, QGridLayout, QMessageBox, QCheckBox, QToolBar, QMenu, QStackedWidget, QApplication, QDialog, QSplitter, QGroupBox, QTextEdit, QTabWidget, QMainWindow, QCheckBox, QStyle, QTreeWidgetItem, QSizePolicy, QScrollBar, QProgressBar, QScrollArea, QDialogButtonBox, QFileDialog) from PyQt5.QtCore import (Qt, pyqtSignal, QTime, QDateTime, QEventLoop, QUrl, QEvent, QObject, QSettings, QSize, pyqtProperty, QTimer, QPropertyAnimation, QEasingCurve, QThread, pyqtSignal, pyqtSlot, QEventLoop, QAbstractAnimation) from PyQt5.QtGui import QFont, QIcon, QPainter, QMovie, QPixmap from ha2 import BrowserWindow, ConsoleHeader from modules.debug_trace import start_trace, stop_trace import sys import json import random import colorsys import time import psutil from psutil import NoSuchProcess, TimeoutExpired import importlib import logging logger = logging.getLogger(__name__) import asyncio import re import ctypes start_trace("module_outlook_2021.py", print_vars=True) # Configuration du dossier de logs LOG_DIR = r"C:\AUTOLOOK\log" os.makedirs(LOG_DIR, exist_ok=True) # Configuration du format de date pour le nom du fichier current_file = os.path.basename(__file__).replace('.py', '') current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") log_file = os.path.join(LOG_DIR, f"module_outlook_2021_{current_time}.txt") # Configuration du logger principal logger = logging.getLogger(__name__) # Logger spécifique au module logger.setLevel(logging.DEBUG) # Formatter personnalisé avec nom de fichier class FileNameFilter(logging.Filter): def filter(self, record): record.filename_only = os.path.basename(record.pathname) return True # Formatter pour tous les handlers formatter = logging.Formatter('%(asctime)s - %(filename_only)s - %(name)s - %(levelname)s - %(message)s') # Ajouter le filtre au logger logger.addFilter(FileNameFilter()) # Handler pour le fichier file_handler = logging.FileHandler(log_file, encoding='utf-8') file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # Handler pour la console avec niveau différent console_handler = logging.StreamHandler(sys.stdout) console_handler.setLevel(logging.INFO) console_handler.setFormatter(formatter) logger.addHandler(console_handler) # Journalisation du démarrage logger.info(f"Démarrage du module Outlook") logger.info(f"Fichier de log créé : {log_file}") # Réduire le niveau de log pour certains modules logging.getLogger('comtypes').setLevel(logging.WARNING) logging.getLogger('win32com').setLevel(logging.WARNING) DEBUG_DIR = r"C:\AUTOLOOK\debug" os.makedirs(DEBUG_DIR, exist_ok=True) BANNER_PATH = r"C:\AUTOLOOK\Banniere" os.makedirs(BANNER_PATH, exist_ok=True) debug_logs = [] def add_debug_log(action, message): timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") debug_logs.append(f"{timestamp} - Action: {action} - Message: {message}") def write_debug_file(): if debug_logs: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") debug_file = os.path.join(DEBUG_DIR, f"debug_{timestamp}.txt") with open(debug_file, "w") as f: for log in debug_logs: f.write(f"{log}\n") debug_logs.clear() class ConnectionIndicator(QWidget): """Widget avec trois voyants lumineux pour indiquer le mode de connexion""" def __init__(self, parent=None): super().__init__(parent) self.setupUI() self.current_mode = None def setupUI(self): """Configure l'interface des voyants en compact horizontal avec affichage du mode""" # Layout horizontal principal layout = QHBoxLayout(self) layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(8) # Espacement compact entre les voyants layout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) # Style de base pour les voyants (plus compacts) indicator_style_off = """ QLabel { background-color: #2c2c2c; border: 2px solid #555555; border-radius: 6px; min-width: 12px; max-width: 12px; min-height: 12px; max-height: 12px; } QToolTip { background-color: #2c3e50; color: #ffffff; border: 1px solid #34495e; border-radius: 3px; padding: 5px; font-size: 10px; font-weight: bold; } """ # Voyant Outlook (COM) - compact self.outlook_indicator = QLabel() self.outlook_indicator.setStyleSheet(indicator_style_off) self.outlook_indicator.setFixedSize(12, 12) self.outlook_indicator.setToolTip("Outlook (COM)") # Voyant IMAP - compact self.imap_indicator = QLabel() self.imap_indicator.setStyleSheet(indicator_style_off) self.imap_indicator.setFixedSize(12, 12) self.imap_indicator.setToolTip("IMAP") # Voyant Thunderbird - compact self.thunderbird_indicator = QLabel() self.thunderbird_indicator.setStyleSheet(indicator_style_off) self.thunderbird_indicator.setFixedSize(12, 12) self.thunderbird_indicator.setToolTip("Thunderbird") # Champ d'affichage dynamique du mode actuel self.mode_display = QLabel("---") self.mode_display.setStyleSheet(""" QLabel { background-color: #1a1a1a; color: #888888; border: 1px solid #444444; border-radius: 4px; padding: 2px 8px; font-size: 9px; font-weight: bold; min-width: 60px; max-width: 60px; text-align: center; } """) self.mode_display.setAlignment(Qt.AlignCenter) self.mode_display.setFixedHeight(16) # Ajouter les voyants et le champ d'affichage layout.addWidget(self.outlook_indicator) layout.addWidget(self.imap_indicator) layout.addWidget(self.thunderbird_indicator) layout.addSpacing(10) # Espacement avant le champ d'affichage layout.addWidget(self.mode_display) # Fixer la taille du widget principal (élargi pour inclure le champ) self.setFixedHeight(20) self.setFixedWidth(220) # Largeur pour 3 voyants + champ d'affichage def set_mode(self, mode): """Active le voyant correspondant au mode et met à jour l'affichage""" # Réinitialiser tous les voyants self.reset_all_indicators() # Style de base pour les tooltips tooltip_style = """ QToolTip { background-color: #2c3e50; color: #ffffff; border: 1px solid #34495e; border-radius: 3px; padding: 5px; font-size: 10px; font-weight: bold; } """ if mode == "outlook": # Style du voyant Outlook outlook_style = """ QLabel { background-color: qradialgradient(cx: 0.5, cy: 0.5, radius: 0.8, fx: 0.3, fy: 0.3, stop: 0 #00ff00, stop: 1 #00aa00); border: 2px solid #00ff00; border-radius: 6px; min-width: 12px; max-width: 12px; min-height: 12px; max-height: 12px; } """ + tooltip_style self.outlook_indicator.setStyleSheet(outlook_style) # Style du champ d'affichage pour Outlook self.mode_display.setText("OUTLOOK") self.mode_display.setStyleSheet(""" QLabel { background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #00ff00, stop: 1 #00aa00); color: #000000; border: 1px solid #00ff00; border-radius: 4px; padding: 2px 8px; font-size: 9px; font-weight: bold; min-width: 80px; max-width: 80px; text-align: center; } """) self.current_mode = "outlook" elif mode == "imap": # Style du voyant IMAP imap_style = """ QLabel { background-color: qradialgradient(cx: 0.5, cy: 0.5, radius: 0.8, fx: 0.3, fy: 0.3, stop: 0 #0080ff, stop: 1 #0050aa); border: 2px solid #0080ff; border-radius: 6px; min-width: 12px; max-width: 12px; min-height: 12px; max-height: 12px; } """ + tooltip_style self.imap_indicator.setStyleSheet(imap_style) # Style du champ d'affichage pour IMAP self.mode_display.setText("IMAP") self.mode_display.setStyleSheet(""" QLabel { background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #0080ff, stop: 1 #0050aa); color: #ffffff; border: 1px solid #0080ff; border-radius: 4px; padding: 2px 8px; font-size: 9px; font-weight: bold; min-width: 80px; max-width: 80px; text-align: center; } """) self.current_mode = "imap" elif mode == "thunderbird": # Style du voyant Thunderbird thunderbird_style = """ QLabel { background-color: qradialgradient(cx: 0.5, cy: 0.5, radius: 0.8, fx: 0.3, fy: 0.3, stop: 0 #ff8000, stop: 1 #aa5000); border: 2px solid #ff8000; border-radius: 6px; min-width: 12px; max-width: 12px; min-height: 12px; max-height: 12px; } """ + tooltip_style self.thunderbird_indicator.setStyleSheet(thunderbird_style) # Style du champ d'affichage pour Thunderbird self.mode_display.setText("THUNDERBIRD") self.mode_display.setStyleSheet(""" QLabel { background-color: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #ff8000, stop: 1 #aa5000); color: #ffffff; border: 1px solid #ff8000; border-radius: 4px; padding: 2px 8px; font-size: 9px; font-weight: bold; min-width: 80px; max-width: 80px; text-align: center; } """) self.current_mode = "thunderbird" def reset_all_indicators(self): """Éteint tous les voyants et remet le champ d'affichage par défaut""" indicator_style_off = """ QLabel { background-color: #2c2c2c; border: 2px solid #555555; border-radius: 6px; min-width: 12px; max-width: 12px; min-height: 12px; max-height: 12px; } QToolTip { background-color: #2c3e50; color: #ffffff; border: 1px solid #34495e; border-radius: 3px; padding: 5px; font-size: 10px; font-weight: bold; } """ # Réinitialiser tous les voyants self.outlook_indicator.setStyleSheet(indicator_style_off) self.imap_indicator.setStyleSheet(indicator_style_off) self.thunderbird_indicator.setStyleSheet(indicator_style_off) # Réinitialiser le champ d'affichage self.mode_display.setText("---") self.mode_display.setStyleSheet(""" QLabel { background-color: #1a1a1a; color: #888888; border: 1px solid #444444; border-radius: 4px; padding: 2px 8px; font-size: 9px; font-weight: bold; min-width: 60px; max-width: 60px; text-align: center; } """) self.current_mode = None def setup_tooltips_style(self): """Configure le style des tooltips pour qu'ils soient visibles""" tooltip_style = """ QToolTip { background-color: #2c3e50; color: #ffffff; border: 1px solid #34495e; border-radius: 3px; padding: 5px; font-size: 10px; font-weight: bold; } """ # Appliquer le style à chaque voyant self.outlook_indicator.setStyleSheet(self.outlook_indicator.styleSheet() + tooltip_style) self.imap_indicator.setStyleSheet(self.imap_indicator.styleSheet() + tooltip_style) self.thunderbird_indicator.setStyleSheet(self.thunderbird_indicator.styleSheet() + tooltip_style) class QCustomEvent(QEvent): def __init__(self, event_type, data): super().__init__(event_type) self._data = data def data(self): return self._data logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) dependencies = [] def update_outlook_dpi_scaling(outlook_widget, scale_factor): """ Applique un facteur d'échelle à l'interface OutlookWidget Args: outlook_widget: L'instance d'OutlookWidget à mettre à l'échelle scale_factor: Le facteur d'échelle à appliquer """ try: logger.info(f"Application du facteur d'échelle {scale_factor} à OutlookWidget") # Mise à l'échelle des polices base_font_size = outlook_widget.font_size if hasattr(outlook_widget, 'font_size') else 9 scaled_font_size = max(8, int(base_font_size * scale_factor)) if hasattr(outlook_widget, 'update_font_size'): outlook_widget.update_font_size(scaled_font_size) logger.info(f"Taille de police mise à jour: {scaled_font_size}") # Mise à l'échelle des widgets importants widgets_to_scale = [ ('mailbox_listbox', 80, 150), # (widget_name, min_height, max_height) ('folder_listbox', 80, 150), ('log_text', 120, 300) ] for widget_name, min_height, max_height in widgets_to_scale: if hasattr(outlook_widget, widget_name): widget = getattr(outlook_widget, widget_name) # Appliquer l'échelle new_min_height = int(min_height * scale_factor) new_max_height = int(max_height * scale_factor) # Limiter les valeurs extrêmes new_min_height = max(50, min(new_min_height, 300)) new_max_height = max(100, min(new_max_height, 600)) # Appliquer les nouvelles dimensions widget.setMinimumHeight(new_min_height) widget.setMaximumHeight(new_max_height) logger.info(f"Widget {widget_name} redimensionné: min={new_min_height}, max={new_max_height}") # Mise à l'échelle du mini navigateur si présent #if hasattr(outlook_widget, 'mini_browser'): # mini_width = int(400 * scale_factor) # Largeur de base: 400 # mini_height = int(300 * scale_factor) # Hauteur de base: 300 # # Limiter aux valeurs raisonnables # mini_width = max(300, min(mini_width, 800)) # mini_height = max(200, min(mini_height, 600)) outlook_widget.mini_browser.setFixedSize(mini_width, mini_height) logger.info(f"Mini navigateur redimensionné: {mini_width}x{mini_height}") # Redimensionner les boutons de contrôle button_names = ['zoom_in_button', 'zoom_out_button', 'toggle_browser_button'] for button_name in button_names: if hasattr(outlook_widget, button_name): button = getattr(outlook_widget, button_name) current_size = button.size() # Appliquer l'échelle aux dimensions new_width = int(current_size.width() * scale_factor) new_height = int(current_size.height() * scale_factor) # Limiter les valeurs new_width = max(20, min(new_width, 100)) new_height = max(20, min(new_height, 50)) button.setFixedSize(new_width, new_height) logger.info(f"Bouton {button_name} redimensionné: {new_width}x{new_height}") # Mettre à jour les marges et espacements des layouts if hasattr(outlook_widget, '_scale_layouts'): outlook_widget._scale_layouts(outlook_widget, scale_factor, scale_factor) logger.info("Layouts redimensionnés") # Stocker le facteur d'échelle pour une utilisation ultérieure outlook_widget.width_ratio = scale_factor outlook_widget.height_ratio = scale_factor return True except Exception as e: logger.error(f"Erreur lors de la mise à l'échelle d'OutlookWidget: {str(e)}") return False def get_config(): return {"installed": True} def setup_interface(main_window): # Configurer la fenêtre pour rester au premier plan main_window.setWindowFlags(main_window.windowFlags() | Qt.WindowStaysOnTopHint) outlook_handler = OutlookHandler(main_window) if outlook_handler.connect(): outlook_widget = OutlookWidget(main_window.main_interface, outlook_handler) main_window.main_interface_layout.addWidget(outlook_widget) outlook_widget.update_mailboxes() main_window.outlook_handler = outlook_handler outlook_handler.outlook_widget = outlook_widget main_window.outlook_widget = outlook_widget outlook_widget.update_font_size(main_window.font_size) outlook_widget.main_window = main_window # Configurer le comportement des fenêtres de navigation setup_browser_windows(outlook_widget) else: main_window.show_error_message("Impossible de se connecter à Outlook") def setup_browser_windows(outlook_widget): """Configure les fenêtres du navigateur.""" if not hasattr(outlook_widget, 'browser_window'): # Navigateur normal outlook_widget.browser_window = BrowserWindow(is_mini=False) if not hasattr(outlook_widget, 'mini_browser'): # Mini navigateur explicitement en mode mini outlook_widget.mini_browser = BrowserWindow(is_mini=True) # Assurer que le mini_browser est bien visible et de la bonne taille outlook_widget.mini_browser.setFixedSize(500, 430) # S'assurer que la barre de navigation de chaque onglet du mini_browser reste cachée if hasattr(outlook_widget.mini_browser, 'tab_widget'): for i in range(outlook_widget.mini_browser.tab_widget.count()): tab = outlook_widget.mini_browser.tab_widget.widget(i) if hasattr(tab, 'navigation_bar'): tab.navigation_bar.hide() tab.navigation_bar.setMaximumHeight(0) tab.navigation_bar.setMinimumHeight(0) outlook_widget.mini_recorder = ActionRecorder(outlook_widget.mini_browser) outlook_widget.full_recorder = ActionRecorder(outlook_widget.browser_window) # S'assurer que les deux navigateurs ont un ConsoleHeader for browser in [outlook_widget.browser_window, outlook_widget.mini_browser]: # Forcer le masquage de la barre de navigation dans le mini_browser if hasattr(outlook_widget, 'mini_browser') and getattr(outlook_widget.mini_browser, 'is_mini', False): tw = getattr(outlook_widget.mini_browser, 'tab_widget', None) if tw: for i in range(tw.count()): tab = tw.widget(i) if hasattr(tab, 'navigation_bar'): tab.navigation_bar.hide() tab.navigation_bar.setMaximumHeight(0) tab.navigation_bar.setMinimumHeight(0) if tab.navigation_bar.parent(): layout = tab.navigation_bar.parent().layout() if layout: try: layout.removeWidget(tab.navigation_bar) except Exception: pass if not hasattr(browser, 'console_header'): # Ne pas ajouter le ConsoleHeader sur le mini_browser si tu ne veux aucun bandeau if getattr(browser, 'is_mini', False): # Optionnel : créé mais caché, ou on ne le crée pas du tout browser.console_header = ConsoleHeader() browser.console_header.hide() else: browser.console_header = ConsoleHeader() layout = browser.layout() if isinstance(layout, (QVBoxLayout, QHBoxLayout)): layout.insertWidget(0, browser.console_header) else: layout.addWidget(browser.console_header) browser.console_header.show() def is_compatible(): try: if sys.version_info < (3, 6): print("Python 3.6 ou supérieur est requis.") return False if platform.system() != "Windows": print("Ce module ne fonctionne que sur Windows.") return False try: app = QApplication.instance() or QApplication([]) except: print("PyQt5 n'est pas installé ou ne fonctionne pas correctement.") return False try: outlook = win32com.client.Dispatch("Outlook.Application") namespace = outlook.GetNamespace("MAPI") folders = namespace.Folders del folders, namespace, outlook except: print("Impossible d'accéder à Outlook ou permissions insuffisantes.") return False print("Le module est compatible avec le système.") return True except Exception as e: print(f"Erreur lors de la vérification de compatibilité : {e}") return False class ActionRecorder: def __init__(self, browser): self.browser = browser # QWebEngineView self.actions = [] self.recording = False self.screenshot_dir = r"C:\\AUTOLOOK\\actions_log" os.makedirs(self.screenshot_dir, exist_ok=True) self.screenshot_index = 0 def start_recording(self): self.recording = True self.actions = [] self._inject_listener_script() def stop_recording(self, callback): self.recording = False self.browser.page().runJavaScript("window.recordedActions", callback) def _inject_listener_script(self): js = """ (function() { window.recordedActions = []; document.addEventListener("click", function(e) { let el = e.target; let info = { type: "click", tag: el.tagName, id: el.id, class: el.className, outerHTML: el.outerHTML, timestamp: Date.now() }; window.recordedActions.push(info); }); document.addEventListener("input", function(e) { let el = e.target; if (el.tagName.toLowerCase() === "input" || el.tagName.toLowerCase() === "textarea") { let info = { type: "input", tag: el.tagName, id: el.id, class: el.className, value: el.value, outerHTML: el.outerHTML, timestamp: Date.now() }; window.recordedActions.push(info); } }); })(); """ self.browser.page().runJavaScript(js) def save_actions_to_json(self, actions): timestamp = QTimer().remainingTime() json_path = os.path.join(self.screenshot_dir, f"actions_{timestamp}.json") with open(json_path, "w", encoding="utf-8") as f: json.dump(actions, f, indent=2) def take_screenshot(self): if self.browser: img = self.browser.grab() path = os.path.join(self.screenshot_dir, f"screenshot_{self.screenshot_index}.png") self.screenshot_index += 1 img.save(path) return path return None def try_replay_last_actions(self): settings = QSettings("Snoown2k", "MyApp") auto_enabled = settings.value("actions/auto_replay_enabled", False, type=bool) if not auto_enabled: return # Chercher le dernier fichier actions_*.json try: files = [f for f in os.listdir(self.screenshot_dir) if f.startswith("actions_") and f.endswith(".json")] if not files: return files.sort(reverse=True) latest = files[0] path = os.path.join(self.screenshot_dir, latest) with open(path, "r", encoding="utf-8") as f: actions = json.load(f) self._replay_actions(actions) except Exception as e: print(f"Erreur lors de la tentative de replay automatique : {e}") def _replay_actions(self, actions): for act in actions: if act["type"] == "click": js = f""" let el = document.querySelector('[id="{act['id']}"]') || document.querySelector('[class~="{act['class']}"]'); if (el) el.click(); """ self.browser.page().runJavaScript(js) elif act["type"] == "input": value = act.get("value", "") js = f""" let el = document.querySelector('[id="{act['id']}"]') || document.querySelector('[class~="{act['class']}"]'); if (el) el.value = "{value}"; """ self.browser.page().runJavaScript(js) class ActionSummaryDialog(QDialog): def __init__(self, actions, parent=None): super().__init__(parent) self.setWindowTitle("Actions enregistrées pendant la pause") self.setMinimumSize(600, 400) self.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel, QListWidget, QCheckBox { color: white; font-size: 12px; } QPushButton { background-color: #008CBA; color: white; padding: 8px 20px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """) layout = QVBoxLayout(self) info_label = QLabel("Voici les actions enregistrées pendant la pause :") layout.addWidget(info_label) self.list_widget = QListWidget() for act in actions: desc = f"{act.get('type').upper()} sur {act.get('tag')}" item = QListWidgetItem(desc) self.list_widget.addItem(item) layout.addWidget(self.list_widget) self.auto_replay_checkbox = QCheckBox("Rejouer automatiquement si page similaire") layout.addWidget(self.auto_replay_checkbox) button_layout = QHBoxLayout() self.reuse_button = QPushButton("Réutiliser ces actions") self.cancel_button = QPushButton("Fermer") button_layout.addWidget(self.reuse_button) button_layout.addWidget(self.cancel_button) layout.addLayout(button_layout) self.reuse_button.clicked.connect(self.accept) self.cancel_button.clicked.connect(self.reject) def get_auto_replay_flag(self): return self.auto_replay_checkbox.isChecked() class AnimatedProgressBar(QProgressBar): def __init__(self, parent=None): super().__init__(parent) # Timer pour l'animation self.animation_timer = QTimer(self) self.animation_timer.timeout.connect(self.update_animation) self.animation_step = 0 self.animation_colors = [] self.generate_animation_colors() self.setStyleSheet(self.generate_style(self.animation_colors[0])) def generate_animation_colors(self): """Génère une palette de couleurs pour l'animation""" base_color = (52, 152, 219) # Bleu accent_color = (41, 128, 185) # Bleu clair steps = 20 self.animation_colors = [] for i in range(steps): factor = abs(math.sin(i / steps * math.pi)) color = tuple(int(base_color[j] + (accent_color[j] - base_color[j]) * factor) for j in range(3)) self.animation_colors.append(color) def generate_style(self, color): """Génère le style CSS pour la barre de progression""" return f""" QProgressBar {{ border: 2px solid #2c3e50; border-radius: 5px; text-align: center; color: white; background-color: #34495e; font-weight: bold; text-align: center; font-size: 12px; }} QProgressBar::chunk {{ background-color: rgb{color}; border-radius: 3px; margin: 0.5px; }} """ def start_animation(self): if not self.animation_timer.isActive(): self.animation_timer.start(50) def stop_animation(self): if self.animation_timer.isActive(): self.animation_timer.stop() def update_animation(self): self.animation_step = (self.animation_step + 1) % len(self.animation_colors) self.setStyleSheet(self.generate_style(self.animation_colors[self.animation_step])) def setValue(self, value): super().setValue(value) if value > 0 and value < 100: self.start_animation() else: self.stop_animation() class ImapHandler: def __init__(self, email, password, provider, log_callback=None): self.email = email self.password = password self.provider = provider self.connection = None self.log_callback = log_callback self.folders_list = [] # Pour stocker la liste des dossiers self.servers = { 'Gmail': ('imap.gmail.com', 993), 'Yahoo': ('imap.mail.yahoo.com', 993), 'Outlook': ('outlook.office365.com', 993), 'AOL': ('imap.aol.com', 993), 'iCloud': ('imap.mail.me.com', 993), 'Orange': ('imap.orange.fr', 993), 'Free': ('imap.free.fr', 993), 'SFR': ('imap.sfr.fr', 993) } def log(self, message): if self.log_callback: self.log_callback(message) print(message) def connect(self): import imaplib import socket try: if self.provider not in self.servers: self.log(f"❌ Fournisseur inconnu: {self.provider}") return False server, port = self.servers[self.provider] self.log(f"🔌 Connexion à {server}:{port}") socket.setdefaulttimeout(20) self.connection = imaplib.IMAP4_SSL(server, port) self.log(f"🔑 Authentification...") self.connection.login(self.email, self.password) self.log("✅ Connexion IMAP réussie") return True except imaplib.IMAP4.error as e: self.log(f"❌ Erreur IMAP: {str(e)}") return False except socket.timeout: self.log("❌ Timeout de connexion") return False except Exception as e: self.log(f"❌ Exception de connexion: {str(e)}") return False def list_folders(self): try: if not self.connection: self.log("⚠️ Aucune connexion active") return [] self.log("📁 Récupération des dossiers IMAP...") status, folders = self.connection.list() if status != 'OK': self.log("❌ Échec récupération dossiers") return [] folder_list = [] for folder in folders: decoded = folder.decode('utf-8') self.log(f" • {decoded}") folder_list.append(decoded) self.folders_list = folder_list return folder_list except Exception as e: self.log(f"❌ Erreur liste dossiers: {e}") return [] def disconnect(self): if self.connection: try: self.connection.logout() self.log("🔚 Déconnexion réussie") except Exception as e: self.log(f"⚠️ Erreur à la déconnexion: {e}") # MÉTHODE CORRIGÉE - Plus de référence circulaire def process_account_data(self, account_data): """ Traite les données d'un compte IMAP déjà sélectionné Args: account_data: dict contenant email, password, folders, etc. Returns: bool: True si traitement réussi """ try: if not isinstance(account_data, dict): self.log(f"❌ Format de données invalide: {type(account_data)}") return False email = account_data.get('email', '') folders = account_data.get('folders', []) self.log(f"📧 Traitement du compte: {email}") self.log(f"📁 Dossiers: {len(folders)}") # Traiter chaque dossier for folder in folders: folder_name = folder.get('name', 'Dossier inconnu') if isinstance(folder, dict) else str(folder) self.log(f" 📂 {folder_name}") # Ici, vous pouvez ajouter la logique de traitement spécifique # Par exemple, mettre à jour l'interface, sauvegarder, etc. return True except Exception as e: self.log(f"❌ Erreur dans process_account_data: {e}") return False # NOUVELLE MÉTHODE pour traiter plusieurs comptes def process_multiple_accounts(self, accounts_list): """ Traite une liste de comptes IMAP Args: accounts_list: list de dict contenant les données des comptes Returns: dict: Résumé du traitement """ try: results = { 'total': len(accounts_list), 'success': 0, 'errors': 0, 'processed_accounts': [] } self.log(f"📨 Traitement de {len(accounts_list)} comptes IMAP") for i, account in enumerate(accounts_list): try: email = account.get('email', f'Compte_{i + 1}') self.log(f"🔄 Traitement {i + 1}/{len(accounts_list)}: {email}") if self.process_account_data(account): results['success'] += 1 results['processed_accounts'].append({ 'email': email, 'status': 'success', 'folders_count': len(account.get('folders', [])) }) else: results['errors'] += 1 results['processed_accounts'].append({ 'email': email, 'status': 'error', 'error': 'Échec du traitement' }) except Exception as e: results['errors'] += 1 results['processed_accounts'].append({ 'email': account.get('email', f'Compte_{i + 1}'), 'status': 'error', 'error': str(e) }) self.log(f"❌ Erreur compte {i + 1}: {str(e)}") # Résumé final self.log(f"✅ Traitement terminé: {results['success']} succès, {results['errors']} erreurs") return results except Exception as e: self.log(f"❌ Erreur dans process_multiple_accounts: {e}") return {'total': 0, 'success': 0, 'errors': 1, 'processed_accounts': []} class OutlookHandler(QObject): scan_progress = pyqtSignal(int) cycle_status_update = pyqtSignal(int, int, int, int) scan_finished = pyqtSignal(int) error_occurred = pyqtSignal(str) log_event = pyqtSignal(str) # Signal pour les logs links_clicked_updated = pyqtSignal(int) def __init__(self, main_window): add_debug_log("OutlookHandler.__init__", "Initialisation de OutlookHandler") super().__init__() self.main_window = main_window self.outlook = None self.reward_value = None self.links_clicked = 0 self.total_reward = 0.0 self.outlook_connected = False self.saved_folder_id = None # Ajouter ces deux attributs pour le TimerWidget self.display_time = 0.0 self.between_time = 0.0 self.browser_window = None self.browser = None self.pause_time = 15 self.links_count = 1 self.should_stop = False self.outlook_widget = None self.is_paused = False self.current_email = None self.links_clicked = 0 self.links_skipped = 0 self.min_pause_time = 15 # Temps minimum par défaut self.max_pause_time = 25 # Temps maximum par défaut self.min_between_pages_time = 5 self.max_between_pages_time = 10 self.auto_delete = True self.links_handler = ProcessedLinksHandler() self.doublons_count = 0 # Nouveau compteur pour les doublons self.cycle_processing_enabled = False # Valeurs fixes (pour rétrocompatibilité) self.emails_per_cycle = 2 self.pause_between_cycles = 5 # Nouvelles valeurs de plages self.min_emails_per_cycle = 2 self.max_emails_per_cycle = 100 self.min_pause_between_cycles = 5 self.max_pause_between_cycles = 2700 self.pattern_keywords = [] self.load_pattern_keywords() # Configuration de la suppression des emails self.permanent_delete_enabled = False self.apply_to_scan = True self.apply_to_count = True self.duplicates_detection_enabled = False # Connexion COM permanente ou ponctuelle self.permanent_connection = self.load_connection_mode() # Ajout du timer keep-alive self.keep_alive_timer = QTimer() self.keep_alive_timer.timeout.connect(self.check_connection) self.keep_alive_timer.start(60) # Vérifie toutes les minutes self.banner_position = "bottom" # Timer de déconnexion automatique self.auto_disconnect_timer = QTimer() self.auto_disconnect_timer.setInterval(10000) # 10 secondes self.auto_disconnect_timer.setSingleShot(True) self.auto_disconnect_timer.timeout.connect(self.auto_disconnect) self.warned_about_disconnect = False def log_action(self, message, category='system'): """ Méthode de logging pour OutlookHandler qui émet un signal vers OutlookWidget """ try: # Émettre le signal log_event vers OutlookWidget if hasattr(self, 'log_event'): self.log_event.emit(message) else: # Fallback : print si le signal n'existe pas print(f"OutlookHandler: {message}") except Exception as e: print(f"Erreur dans OutlookHandler.log_action: {e}") print(f"Message original: {message}") def safe_com_operation(self, operation, max_retries=3, delay=1.0): """ Exécute une opération COM de manière sécurisée avec retry """ for attempt in range(max_retries): try: return operation() except Exception as e: error_str = str(e) if "0xc0000005" in error_str or "RPC" in error_str or "COM" in error_str: if attempt < max_retries - 1: self.log_action(f"⚠️ Erreur COM détectée (tentative {attempt + 1}/{max_retries}), retry...") time.sleep(delay) # Tentative de nettoyage COM try: import gc gc.collect() pythoncom.CoFreeUnusedLibraries() except: pass continue else: self.log_action(f"❌ Échec définitif après {max_retries} tentatives: {error_str}") raise else: # Si ce n'est pas une erreur COM, on relance immédiatement raise return None def load_connection_mode(self): settings = QSettings("Snoown2k", "MyApp") return settings.value("outlook/permanent_connection", False, type=bool) def save_connection_mode(self, permanent: bool): self.permanent_connection = permanent settings = QSettings("Snoown2k", "MyApp") settings.setValue("outlook/permanent_connection", permanent) settings.sync() def ensure_connected(self): """ S'assure qu'Outlook est connecté avec gestion COM renforcée """ try: # Si déjà connecté, vérifier la santé COM if self.outlook_connected and self.outlook: try: # Test rapide COM _ = self.outlook.GetDefaultFolder(6).Items.Count self.log_action("✅ Connexion COM healthy") return True except Exception as com_error: self.log_action(f"⚠️ COM unhealthy: {str(com_error)}, reconnexion...") # Nettoyage forcé try: del self.outlook pythoncom.CoUninitialize() except: pass self.outlook = None self.outlook_connected = False # Reconnexion avec délai self.log_action("🔌 Initialisation COM...") time.sleep(0.5) # Petit délai pour stabiliser try: pythoncom.CoInitialize() time.sleep(0.2) # Délai pour l'initialisation self.log_action("🔗 Connexion à Outlook...") outlook_app = win32com.client.Dispatch("Outlook.Application") time.sleep(0.3) # Délai pour Dispatch self.outlook = outlook_app.GetNamespace("MAPI") # Test de connexion test_folder = self.outlook.GetDefaultFolder(6) test_count = test_folder.Items.Count self.outlook_connected = True self.log_action(f"✅ COM initialisé - {test_count} emails en boîte de réception") return True except Exception as e: self.log_action(f"❌ Échec initialisation COM: {str(e)}") return False except Exception as e: self.log_action(f"❌ Erreur ensure_connected: {str(e)}") return False def load_pattern_keywords(self): try: # Créer le répertoire Profiles s'il n'existe pas os.makedirs("Keywords", exist_ok=True) filepath = os.path.join("Keywords", "pattern_keywords.json") if os.path.exists(filepath): with open(filepath, "r", encoding="utf-8") as f: self.pattern_keywords = json.load(f) else: # Créer une liste par défaut avec quelques mots-clés d'exemple self.pattern_keywords = ["view", "confirm", "click", "activate", "verify"] # Sauvegarder immédiatement self.save_pattern_keywords() except Exception as e: print(f"Erreur chargement mots-clés pattern: {e}") self.pattern_keywords = ["view", "confirm", "click", "activate", "verify"] def reset_scan_stats(self): """Réinitialise les statistiques de scan""" try: self.links_clicked = 0 self.links_skipped = 0 self.total_reward = 0.0 self.doublons_count = 0 self.log_action("🔄 Statistiques de scan réinitialisées") except Exception as e: self.log_action(f"❌ Erreur lors de la réinitialisation des stats : {e}") def _get_reward_value(self): """Récupère la valeur de récompense actuelle""" try: return self.reward_value if self.reward_value is not None else 0.0 except Exception as e: self.log_action(f"❌ Erreur récupération reward_value : {e}") return 0.0 def _get_max_emails(self): """Récupère le nombre maximum d'emails à traiter depuis l'interface""" try: if hasattr(self, 'outlook_widget') and self.outlook_widget: max_emails_text = self.outlook_widget.max_emails_entry.text().strip() if max_emails_text and max_emails_text.isdigit(): return int(max_emails_text) # Valeur par défaut si rien n'est configuré return 50 except Exception as e: self.log_action(f"❌ Erreur récupération max_emails : {e}") return 50 def save_pattern_keywords(self): try: # Créer le répertoire s'il n'existe pas os.makedirs("Keywords", exist_ok=True) filepath = os.path.join("Keywords", "pattern_keywords.json") with open(filepath, "w", encoding="utf-8") as f: json.dump(self.pattern_keywords, f, ensure_ascii=False, indent=2) except Exception as e: print(f"Erreur sauvegarde mots-clés pattern: {e}") def add_pattern_keyword(self, keyword): """ Ajoute un mot-clé s’il n’existe pas déjà et le sauvegarde. """ try: keyword = keyword.strip().lower() if keyword and keyword not in self.pattern_keywords: self.pattern_keywords.append(keyword) self.save_pattern_keywords() self.log_action(f"➕ Mot-clé ajouté : {keyword}") else: self.log_action(f"ℹ️ Mot-clé déjà présent ou vide : {keyword}") except Exception as e: self.log_action(f"❌ Erreur lors de l'ajout du mot-clé : {e}") def _safe_get_body(self, message): """ Accès ultra-sécurisé au Body ou HTMLBody, sans dépasser une taille max. Nettoie les caractères exotiques pour éviter les débordements. """ MAX_BODY_LENGTH = 10000 try: body_str = "" if hasattr(message, 'Body'): try: body = message.Body if body: body_str = str(body) self.log_action(f"📝 Body récupéré (brut), longueur : {len(body_str)}") except Exception as e: self.log_action(f"⚠ Erreur lecture Body : {e}") if not body_str and hasattr(message, 'HTMLBody'): try: html_body = message.HTMLBody if html_body: body_str = str(html_body) self.log_action(f"📝 HTMLBody récupéré (brut), longueur : {len(body_str)}") except Exception as e: self.log_action(f"⚠ Erreur lecture HTMLBody : {e}") if body_str: # Nettoyer les caractères non ASCII (évite les surprises dans les regex et slices) body_str = re.sub(r'[^\x00-\x7F]+', ' ', body_str) body_str = body_str[:MAX_BODY_LENGTH] return body_str except Exception as e: self.log_action(f"❌ Erreur globale accès body : {e}") return "" def find_valid_link(self, message, link_keywords, mode="url"): """ Méthode unifiée pour trouver un lien valide dans un message Args: message: Le message Outlook à analyser link_keywords: Liste des mots-clés à rechercher mode: "url" pour mode classique, "pattern" pour mode pattern Returns: str: URL du lien trouvé ou None """ try: # Déterminer le mode à utiliser if hasattr(self, 'link_mode'): mode = self.link_mode if mode == "pattern": # Utiliser la méthode pattern existante return self.find_valid_link_pattern(message, link_keywords) else: # Mode URL classique - utiliser la méthode optimisée if hasattr(self, '_find_valid_link_optimized'): # Convertir link_keywords en set pour la méthode optimisée keyword_cache = set(kw.lower() for kw in link_keywords) return self._find_valid_link_optimized(message, keyword_cache) else: # Fallback vers la méthode pattern si optimisée n'existe pas return self.find_valid_link_pattern(message, link_keywords) except Exception as e: self.log_action(f"❌ Erreur dans find_valid_link: {str(e)}") return None def find_valid_link_pattern(self, message, link_keywords): """ Recherche un lien valide dans le message en mode pattern uniquement (analyse du contexte). Validation stricte : doit contenir au moins un mot-clé pattern pour valider le lien. """ import gc import pythoncom import json import os import re self.log_action("🧭 Mode PATTERN - Analyse contextuelle activée avec validation stricte") exclude_keywords = ['delete', 'unsubscribe', 'remove', 'vacation', 'report', 'login', 'helpdesk', 'support', 'assistance'] try: # Charger les mots-clés pattern depuis le fichier JSON pattern_keywords = [] try: keywords_file = r"c:\autolook\keywords\pattern_keywords.json" if os.path.exists(keywords_file): with open(keywords_file, 'r', encoding='utf-8') as f: pattern_keywords = json.load(f) self.log_action(f"📋 {len(pattern_keywords)} mots-clés pattern chargés depuis {keywords_file}") else: self.log_action(f"⚠️ Fichier {keywords_file} non trouvé, utilisation des mots-clés par défaut") # Mots-clés par défaut si le fichier n'existe pas pattern_keywords = ["view", "confirm", "click", "activate", "verify", "earn", "credit", "credits", "receive", "below", "claim", "reward", "rewards", "earning", "here", "link", "get", "reading", "email", "open"] except Exception as e: self.log_action(f"❌ Erreur lors du chargement des mots-clés pattern : {e}") # Mots-clés par défaut en cas d'erreur pattern_keywords = ["view", "confirm", "click", "activate", "verify", "earn", "credit", "credits", "receive", "below", "claim", "reward", "rewards", "earning", "here", "link", "get", "reading", "email", "open"] if not pattern_keywords: self.log_action("❌ Aucun mot-clé pattern disponible pour l'analyse") return None # Vérifier la stabilité COM try: _ = str(getattr(message, 'Subject', 'Sujet inconnu')) except Exception as e: self.log_action(f"⚠ Message COM instable, ignoré : {e}") return None # Lire le body en mode sécurisé try: message_body = self._safe_get_body(message) if not message_body or len(message_body.strip()) == 0: self.log_action("📭 Body vide ou inaccessible, aucun traitement possible.") return None except Exception as e: self.log_action(f"❌ Erreur lors de la lecture du body : {e}") return None # Nettoyer le body des caractères exotiques try: safe_body = re.sub(r'[^\x00-\x7F]+', ' ', message_body) # Normaliser les espaces safe_body = re.sub(r'\s+', ' ', safe_body) except Exception as e: self.log_action(f"❌ Erreur lors du nettoyage du body : {e}") return None self.log_action(f"🔍 Mots-clés pattern de référence : {pattern_keywords}") self.log_action(f"📏 Taille du body analysé : {len(safe_body)} caractères") # Extraire tous les liens du message try: links_iter = re.finditer(r'http[s]?://[^\s<>"\']+', safe_body) except Exception as e: self.log_action(f"❌ Erreur lors de l'extraction des liens : {e}") return None processed_links = 0 valid_link_found = False for match in links_iter: processed_links += 1 try: link = match.group() clean_link = link.strip().rstrip('<>"\'') try: parsed = urlparse(clean_link) domain = parsed.netloc.lower() except Exception as e: self.log_action(f"⚠ Erreur parsing lien #{processed_links} : {clean_link} - {str(e)}") continue self.log_action(f"🔗 Analyse lien #{processed_links} : {clean_link}") # Vérifier les mots-clés d'exclusion excluded_keywords_found = [ex_kw for ex_kw in exclude_keywords if ex_kw.lower() in clean_link.lower()] if excluded_keywords_found: self.log_action(f"⛔ Lien exclu (mots-clés d'exclusion détectés) : {excluded_keywords_found}") continue # PREMIÈRE MÉTHODE : Vérifier si des mots-clés sont directement dans le lien keywords_in_link = [] for keyword in pattern_keywords: if keyword.lower() in clean_link.lower(): keywords_in_link.append(keyword) if keywords_in_link: self.log_action( f"🔑 Mots-clés de référence détectés dans le lien : {', '.join(keywords_in_link)}") self.log_action(f"💬 Validation : lien contenant directement les mots-clés de référence") self.log_action(f"✅ Lien valide trouvé (mots-clés de référence dans le lien) : {clean_link}") return clean_link # DEUXIÈME MÉTHODE : Analyse du contexte avant le lien start_ctx = max(0, match.start() - 300) # Augmenter la zone de contexte à 300 caractères if match.start() > len(safe_body): self.log_action("⚠ Lien trouvé hors des limites du body, ignoré.") continue try: context_snippet = safe_body[start_ctx:match.start()] context_snippet = re.sub(r'\s+', ' ', context_snippet) # normaliser les espaces context_lower = context_snippet.lower() # Afficher le contexte pour debug context_display = context_snippet[-100:] if len(context_snippet) > 100 else context_snippet self.log_action(f"📝 Contexte analysé : '{context_display.strip()}'") except Exception as e: self.log_action(f"❌ Erreur lors de l'extraction du contexte : {e}") continue # Chercher les mots-clés de référence dans le contexte avec validation stricte found_keywords = [] for keyword in pattern_keywords: # Recherche de mots complets pour éviter les faux positifs pattern = r'\b' + re.escape(keyword.lower()) + r'\b' if re.search(pattern, context_lower): found_keywords.append(keyword) # Validation stricte : doit contenir au moins un mot-clé de référence if not found_keywords: self.log_action( f"❌ Aucun mot-clé de référence trouvé dans le contexte du lien #{processed_links}") self.log_action(f"⚠️ Mots-clés recherchés : {', '.join(pattern_keywords)}") self.log_action(f"📋 Contexte analysé sans succès : '{context_display.strip()}'") continue # Extraire et loguer la phrase contenant le mot-clé matched_sentence = None try: # Diviser le contexte en phrases sentences = re.split(r'(?<=[.!?:])\s+', context_snippet.strip()) # Chercher la phrase contenant au moins un mot-clé for sentence in sentences: sentence_lower = sentence.lower() sentence_keywords = [] for keyword in found_keywords: pattern = r'\b' + re.escape(keyword.lower()) + r'\b' if re.search(pattern, sentence_lower): sentence_keywords.append(keyword) if sentence_keywords: matched_sentence = sentence.strip() self.log_action( f"💬 Phrase validée avec mots-clés de référence : \"{matched_sentence}\"") self.log_action(f"🎯 Mots-clés de référence validés : {', '.join(sentence_keywords)}") break if not matched_sentence: # Si aucune phrase complète n'est trouvée, utiliser le contexte complet self.log_action( f"💬 Contexte validé avec mots-clés de référence : \"{context_display.strip()}\"") self.log_action(f"🎯 Mots-clés de référence validés : {', '.join(found_keywords)}") except Exception as e: self.log_action(f"⚠ Erreur lors de l'extraction de phrase : {e}") self.log_action(f"💬 Contexte validé : \"{context_display.strip()}\"") self.log_action(f"🎯 Mots-clés de référence validés : {', '.join(found_keywords)}") # Lien validé avec mots-clés de référence self.log_action(f"✅ VALIDATION RÉUSSIE - Lien approuvé : {clean_link}") self.log_action(f"🔑 Mots-clés de référence confirmés : {', '.join(found_keywords)}") return clean_link except Exception as e: self.log_action(f"❌ Erreur traitement lien #{processed_links} : {e}") continue # Nettoyage périodique try: if processed_links % 10 == 0: # Nettoyer tous les 10 liens pythoncom.CoFreeUnusedLibraries() gc.collect() except Exception as e: self.log_action(f"⚠ Erreur lors du nettoyage mémoire : {e}") # Aucun lien valide trouvé if processed_links == 0: self.log_action(f"❌ Aucun lien trouvé dans le message") else: self.log_action(f"❌ MOTS-CLÉS INTROUVABLES - {processed_links} liens analysés") self.log_action(f"⚠️ Aucun lien ne contient les mots-clés de référence requis") self.log_action(f"📋 Mots-clés recherchés : {', '.join(pattern_keywords)}") except Exception as e: self.log_action(f"❌ Erreur globale find_valid_link_pattern : {e}") finally: # Nettoyage final try: pythoncom.CoFreeUnusedLibraries() gc.collect() except Exception as e: self.log_action(f"⚠ Erreur lors du nettoyage final : {e}") return None def _safe_find_link_without_context(self, message, link_keywords, exclude_keywords): """ Fallback : extraction ultra-sécurisée sans contexte, uniquement sur l'URL. """ try: body = self._safe_get_body(message) if not body: self.log_action("Fallback : body vide") return None if len(body) > 5000: body = body[:5000] self.log_action(f"Fallback : body récupéré (début) : {body[:200]}") links = re.findall(r'http[s]?://[^\s<>"\']+', body) self.log_action(f"Fallback : liens extraits : {links}") for link in links[:8]: clean_link = link.strip().rstrip('<>"\'') # Vérifie si un mot-clé est directement dans le lien keywords_in_link = [kw for kw in pattern_keywords if kw.lower() in clean_link.lower()] if keywords_in_link: self.log_action(f"🔑 Mot-clé détecté dans le lien : {keywords_in_link}") self.log_action(f"💬 Phrase détectée : lien contenant directement le mot-clé") self.log_action(f"✅ Lien valide trouvé (mot-clé dans le lien) : {clean_link}") return clean_link if any(kw in clean_link.lower() for kw in link_keywords): if not any(ex_kw in clean_link.lower() for ex_kw in exclude_keywords): self.log_action(f"Fallback : lien trouvé : {clean_link}") return clean_link except Exception as e: self.log_action(f"Erreur fallback : {str(e)}") return None def set_banner_position(self, position): """Met à jour la position de la bannière et log l'action""" old_position = getattr(self, 'banner_position', 'unknown') self.banner_position = position self.log_action(f"Bandeau position mis à jour : {old_position} -> {position}") # Vérification de la synchronisation avec l'interface if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'main_window'): main_window = self.outlook_widget.main_window if hasattr(main_window, 'banner_position_combo'): # Synchroniser le combo avec la nouvelle valeur position_map = {"bottom": 0, "top": 1, "disabled": 2} if position in position_map: combo_index = position_map[position] if main_window.banner_position_combo.currentIndex() != combo_index: main_window.banner_position_combo.setCurrentIndex(combo_index) self.log_action(f"Combo synchronisé avec index: {combo_index}") def update_mini_browser_banner(self, subject): self.log_action(f"🔍 update_mini_browser_banner - Position: {getattr(self, 'banner_position', 'undefined')}") """ Met à jour le bandeau du mini navigateur avec le sujet du mail en cours de traitement. """ try: if hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser # Vérifier d'abord console_header if hasattr(browser, 'console_header') and hasattr(browser.console_header, 'set_message'): browser.console_header.set_message(subject) self.log_action(f"🧠 Sujet transmis au console_header : {subject}") return # Ensuite vérifier bottom_banner if hasattr(browser, 'bottom_banner') and hasattr(browser.bottom_banner, 'show_banner'): browser.bottom_banner.show_banner(f"📧 {subject}") self.log_action(f"🧠 Sujet transmis au bottom_banner : {subject}") return # Si aucune bannière n'est disponible self.log_action(f"⚠️ Aucune bannière disponible pour afficher : {subject}") except Exception as e: self.log_action(f"⚠️ Erreur mise à jour bandeau : {e}") def set_duplicates_detection(self, enabled): """Active ou désactive la détection des doublons""" self.duplicates_detection_enabled = enabled if enabled: self.log_action("✅ Détection des doublons activée") else: self.log_action("❌ Détection des doublons désactivée") def check_connection(self): """Vérifie si la connexion COM est encore active, sinon ne fait rien si elle a été volontairement coupée.""" # Initialiser les flags une seule fois if not hasattr(self, 'warned_about_disconnect'): self.warned_about_disconnect = False if not hasattr(self, 'warned_about_rpc_error'): self.warned_about_rpc_error = False try: # Connexion volontairement arrêtée (via auto_disconnect) if self.outlook is None: if not self.warned_about_disconnect: self.log_action("🔕 Connexion COM inactive — aucune tentative de reconnexion (mode veille)") self.warned_about_disconnect = True return False # Test simple de la session COM _ = self.outlook.GetDefaultFolder(6).Items.Count # Réinitialiser les warnings si tout va bien self.warned_about_disconnect = False self.warned_about_rpc_error = False return True except Exception: if not self.warned_about_rpc_error: self.log_action("⚠️ Connexion RPC perdue") self.warned_about_rpc_error = True return False def initialize_cycle_parameters(self): """Initialise les paramètres de cycle""" self.cycle_processing_enabled = False self.min_emails_per_cycle = 2 self.max_emails_per_cycle = 100 self.min_pause_between_cycles = 5 self.max_pause_between_cycles = 2700 logger.info("Paramètres de cycle initialisés") def reconnect(self): try: pythoncom.CoUninitialize() # Nettoyage de l'ancienne connexion self.connect() except Exception as e: logger.error(f"Échec de la reconnexion : {e}") def safe_rpc_call(self, func): max_retries = 3 retry_count = 0 while retry_count < max_retries: try: return func() except Exception as e: retry_count += 1 logger.warning(f"Erreur RPC (tentative {retry_count}/{max_retries}): {e}") if retry_count < max_retries: self.reconnect() else: raise def set_cycle_processing(self, enabled): """Active ou désactive le traitement par cycle""" self.cycle_processing_enabled = enabled logger.info(f"Traitement par cycle dans OutlookHandler: {'activé' if enabled else 'désactivé'}") def set_emails_per_cycle(self, count): """ Définit le nombre d'emails à traiter par cycle Pour la rétrocompatibilité, on peut passer un entier simple ou une plage (min, max) """ if isinstance(count, tuple) and len(count) == 2: self.min_emails_per_cycle = count[0] self.max_emails_per_cycle = count[1] # Vérifie si la nouvelle plage est différente de la précédente if getattr(self, '_last_emails_per_cycle', None) != (self.min_emails_per_cycle, self.max_emails_per_cycle): self._last_emails_per_cycle = (self.min_emails_per_cycle, self.max_emails_per_cycle) # N'affiche le log que si le mode cycle est activé if getattr(self, 'cycle_processing_enabled', False): self.log_action( f"📊 Plage d'emails par cycle définie: {self.min_emails_per_cycle}-{self.max_emails_per_cycle}") else: # Ancienne version avec une valeur fixe self.emails_per_cycle = count self.min_emails_per_cycle = count self.max_emails_per_cycle = count # Vérifie si la valeur a changé avant de logguer if getattr(self, '_last_emails_per_cycle', None) != (count, count): self._last_emails_per_cycle = (count, count) if getattr(self, 'cycle_processing_enabled', False): self.log_action(f"📊 Nombre fixe d'emails par cycle défini: {count}") def set_pause_between_cycles(self, seconds): """ Définit le temps de pause entre les cycles en secondes Pour la rétrocompatibilité, on peut passer un entier simple ou une plage (min, max) """ if isinstance(seconds, tuple) and len(seconds) == 2: self.min_pause_between_cycles = seconds[0] self.max_pause_between_cycles = seconds[1] # Définir une valeur moyenne pour la rétrocompatibilité self.pause_between_cycles = (self.min_pause_between_cycles + self.max_pause_between_cycles) // 2 logger.info( f"Plage de temps de pause entre cycles définie à {self.min_pause_between_cycles}-{self.max_pause_between_cycles} secondes") else: # Ancienne version avec une valeur fixe self.pause_between_cycles = seconds self.min_pause_between_cycles = seconds self.max_pause_between_cycles = seconds logger.info(f"Temps de pause entre cycles défini à {seconds} secondes") def get_random_emails_per_cycle(self): """Retourne un nombre aléatoire d'emails à traiter dans la plage définie""" # Vérification explicite des limites définies min_count = getattr(self, 'min_emails_per_cycle', 2) max_count = getattr(self, 'max_emails_per_cycle', 6) # Assurer que nous avons des valeurs raisonnables et que min <= max min_count = max(1, min_count) max_count = max(min_count, max_count) # Générer un nombre aléatoire dans la plage définie count = random.randint(min_count, max_count) # Log explicite pour le débogage self.log_action(f"🎲 Nombre d'emails pour ce cycle: {count} (plage configurée: {min_count}-{max_count})") return count def get_random_pause_between_cycles(self): """Retourne un temps de pause aléatoire dans la plage définie""" import random pause_time = random.uniform(self.min_pause_between_cycles, self.max_pause_between_cycles) logger.info(f"Temps de pause entre cycles généré: {pause_time:.2f} secondes") return pause_time def disable_report_toggle(self): if hasattr(self.outlook_widget.main_window, 'report_toggle'): self.outlook_widget.main_window.report_toggle.setChecked(False) if hasattr(self.outlook_widget.main_window, 'task_scheduler_dialog') and \ hasattr(self.outlook_widget.main_window.task_scheduler_dialog, 'report_checkbox'): self.outlook_widget.main_window.task_scheduler_dialog.report_checkbox.setChecked(False) def set_pause_time_range(self, min_time, max_time): # print(f"=== TRACE set_pause_time_range === min={min_time}, max={max_time}") """Définit la plage de temps de pause""" self.min_pause_time = min_time self.max_pause_time = max_time # Mise à jour de display_time pour que TimerWidget soit en synchronisation self.display_time = (min_time + max_time) / 2 # Mettre à jour le TimerWidget si disponible if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'timer_widget'): if not (hasattr(self.outlook_widget, 'is_loading_profile') and self.outlook_widget.is_loading_profile): self.outlook_widget.timer_widget.set_text(f"Display {self.display_time:.1f}s", "display") else: print(f"DEBUG: set_text ignoré par set_pause_time_range pendant chargement profil.") # Vérification de sécurité pour éviter les erreurs COM if not hasattr(self, 'outlook_widget') or self.outlook_widget is None: return logger.info(f"Plage de temps de pause définie: {min_time}-{max_time} secondes") def get_random_pause_time(self): """Retourne un temps de pause aléatoire dans la plage définie""" import random pause_time = random.uniform(self.min_pause_time, self.max_pause_time) logger.info(f"Temps de pause généré: {pause_time:.2f} secondes") return pause_time def _format_log_message(self, message, is_duplicate=False): """Ajoute les icônes appropriées aux messages de log""" # Si c'est un doublon, utiliser un formatage spécial if is_duplicate: return f"🔍 {message}" """Ajoute les icônes appropriées aux messages de log""" # Dictionnaire de correspondance texte -> emoji emoji_map = { "Début du scan": "🔄 ", "Début du comptage": "🔄 ", "Erreur": "❌ ", "Nouveau traitement": "✨ ", "Email ignoré": "⏩ ", "Scan terminé": "🏁 ", "Email supprimé": "🗑️ ", "Progression": "📊 ", "Recherche": "🔍 ", "Email": "📧 ", "Traitement": "⚙️ ", "Lien": "🔗 ", "Pause": "⏸️ ", "Reprise": "▶️ ", "Statistiques": "📈 ", "Validation": "✅ ", "Attention": "⚠️ ", "Information": "ℹ️ " } # Parcourir la map et remplacer les textes par leurs emojis formatted = message for key, emoji in emoji_map.items(): if key in message: if not any(e in formatted for e in emoji_map.values()): formatted = f"{emoji} {formatted}" break if not any(emoji in formatted for emoji in emoji_map.values()): formatted = f"ℹ️ {formatted}" return formatted def set_font_size(self, size): print(f"Taille de police mise à jour : {size}") def _save_pause_state(self): """Sauvegarde UNIQUEMENT les données nécessaires""" try: self.pause_state = { 'remaining_time': getattr(self, 'remaining_time', 0), 'current_progress': self.progress_bar.value() if hasattr(self, 'progress_bar') else 0, 'is_scanning': self.is_scanning, 'is_counting': self.is_counting, 'pause_timestamp': time.time() } # NE PAS toucher aux navigateurs - les laisser tranquilles self.log_action("💾 État sauvegardé (sans manipulation navigateur)") except Exception as e: self.log_action(f"❌ Erreur sauvegarde état: {e}") def _restore_pause_state(self): """Restaure UNIQUEMENT les données Python""" try: if hasattr(self, 'pause_state'): state = self.pause_state # Restaurer seulement les états Python self.remaining_time = state.get('remaining_time', 0) if hasattr(self, 'progress_bar'): self.progress_bar.setValue(state.get('current_progress', 0)) # Calculer le temps écoulé pause_duration = time.time() - state.get('pause_timestamp', 0) self.log_action(f"⏱️ Pause de {pause_duration:.1f} secondes") del self.pause_state # NE PAS toucher aux navigateurs - ils sont restés actifs self.log_action("🔄 État Python restauré (navigateurs intacts)") except Exception as e: self.log_action(f"❌ Erreur restauration état: {e}") def pause_operation(self): """Met en pause COMPLÈTEMENT toutes les opérations""" self.is_paused = True self.log_action("🔴 PAUSE SYSTÈME COMPLÈTE") # Arrêter immédiatement tous les processus lourds try: import threading # Marquer tous les threads pour arrêt for thread in threading.enumerate(): if hasattr(thread, '_stop_requested'): thread._stop_requested = True except Exception as e: self.log_action(f"❌ Erreur pause système: {e}") def resume_operation(self): """Reprend les opérations après pause système""" self.is_paused = False self.log_action("🟢 REPRISE SYSTÈME COMPLÈTE") def update_links_clicked(self, increment=1): self.links_clicked += increment if self.reward_value is not None: self.total_reward = self.reward_value * self.links_clicked self.links_clicked_updated.emit(self.links_clicked) def skip_current_email(self): if self.current_email: self.log_action(f"Email ignoré : {self.current_email.Subject}") self.links_skipped += 1 self.links_clicked_updated.emit(self.links_clicked - self.links_skipped) self.current_email = None def stop_scan(self): self.should_stop = True def get_folder_by_id(self, folder_id): """Récupère un dossier Outlook par son ID""" try: if not self.outlook: self.connect() # Parcourir toutes les boîtes mail for mailbox in self.outlook.Folders: # Rechercher récursivement dans les dossiers folder = self._find_folder_by_id(mailbox, folder_id) if folder: return folder return None except Exception as e: print(f"Erreur lors de la récupération du dossier par ID : {str(e)}") return None def _find_folder_by_id(self, folder, target_id): """Recherche récursive d'un dossier par son ID""" try: # Vérifier si c'est le dossier recherché if folder.EntryID == target_id: return folder # Parcourir les sous-dossiers for subfolder in folder.Folders: result = self._find_folder_by_id(subfolder, target_id) if result: return result return None except Exception as e: print(f"Erreur lors de la recherche du dossier : {str(e)}") return None def get_folder_stats(self, folder_id): """Récupère les statistiques d'un dossier""" try: folder = self.get_folder_by_id(folder_id) if not folder: raise Exception("Dossier non trouvé") items = folder.Items total_count = items.Count # Statistiques de base stats = { 'folder_name': folder.Name, 'total_messages': total_count, 'unread_messages': len([item for item in items if not item.UnRead]), 'time_stats': { 'oldest_message': items[0].ReceivedTime if total_count > 0 else None, 'newest_message': items[total_count - 1].ReceivedTime if total_count > 0 else None }, 'size_stats': { 'total_size': sum(item.Size for item in items), 'average_size': sum(item.Size for item in items) / total_count if total_count > 0 else 0 }, 'sender_stats': self._calculate_sender_stats(items), 'category_stats': self._calculate_category_stats(items) } return stats except Exception as e: print(f"Erreur lors de la récupération des statistiques : {str(e)}") raise def _calculate_sender_stats(self, items): """Calcule les statistiques par expéditeur""" sender_counts = {} try: for item in items: sender = getattr(item, 'SenderEmailAddress', 'Unknown') sender_counts[sender] = sender_counts.get(sender, 0) + 1 return sender_counts except Exception as e: print(f"Erreur lors du calcul des statistiques d'expéditeur : {str(e)}") return {} def _calculate_category_stats(self, items): """Calcule les statistiques par catégorie""" category_counts = {} try: for item in items: categories = getattr(item, 'Categories', '').split(', ') for category in categories: if category: category_counts[category] = category_counts.get(category, 0) + 1 return category_counts except Exception as e: print(f"Erreur lors du calcul des statistiques de catégorie : {str(e)}") return {} def load_browser_module(self): """ Charge le navigateur et initialise les paramètres nécessaires. """ try: # Initialisation de BrowserWindow self.browser = BrowserWindow() # Configuration supplémentaire si nécessaire profile = self.browser.page().profile() if profile: # Configurer les cookies, stockage, et autres réglages via ha2.py setup_cookies_and_storage(profile) setup_javascript_settings(self.browser.page()) set_random_user_agent(profile) logger.info("Navigateur chargé et configuré avec succès.") except Exception as e: logger.error(f"Erreur lors du chargement du navigateur : {str(e)}") def connect(self): if self.outlook: self.log_action("ℹ️ Outlook déjà connecté") return True # CORRECTION: Ne réinitialiser le timer que si pas en mode permanent if not self.permanent_connection: self.reset_auto_disconnect_timer() else: self.log_action("🔒 Mode permanent - Pas de timer de déconnexion") try: add_debug_log("OutlookHandler.connect", "Début de la connexion à Outlook") if self.outlook: try: pythoncom.CoUninitialize() except: pass self.outlook = None pythoncom.CoInitialize() add_debug_log("OutlookHandler.connect", "CoInitialize réussi") outlook_app = win32com.client.Dispatch("Outlook.Application") add_debug_log("OutlookHandler.connect", "Dispatch Outlook.Application réussi") self.outlook = outlook_app.GetNamespace("MAPI") add_debug_log("OutlookHandler.connect", "GetNamespace MAPI réussi") # Vérification immédiate de la connexion _ = self.outlook.GetDefaultFolder(6) # CORRECTION: Démarrage du monitoring seulement en mode ponctuel if not self.permanent_connection: self.keep_alive_timer.start() else: self.log_action("🔒 Connexion permanente établie - Pas de déconnexion automatique") self.outlook_connected = True return True except Exception as e: add_debug_log("OutlookHandler.connect", f"Erreur de connexion à Outlook: {e}") write_debug_file() print(f"Erreur de connexion à Outlook: {e}", file=sys.stderr) return False def reset_auto_disconnect_timer(self): """Redémarre le timer de déconnexion automatique""" # CORRECTION: Ne démarrer le timer que si on n'est pas en mode permanent if self.permanent_connection: self.log_action("🔒 Mode permanent - Timer de déconnexion désactivé") if self.auto_disconnect_timer.isActive(): self.auto_disconnect_timer.stop() return if self.auto_disconnect_timer.isActive(): self.auto_disconnect_timer.stop() self.auto_disconnect_timer.start() self.log_action("⏳ Déconnexion Outlook dans 10 secondes sans activité...") def set_dashboard_reference(self, dashboard): """Définit une référence vers le tableau de bord système""" self.dashboard = dashboard self.log_action("🔗 Référence vers le tableau de bord établie") def notify_connection_change(self): """Notifie le changement d'état de connexion au dashboard""" try: if hasattr(self, 'dashboard') and self.dashboard: if hasattr(self.dashboard, 'update_com_mode_label'): self.dashboard.update_com_mode_label() self.log_action("📡 État COM mis à jour dans le dashboard") except Exception as e: self.log_action(f"⚠️ Erreur notification dashboard : {e}") def auto_disconnect(self): """Déconnecte Outlook automatiquement après inactivité""" try: # CORRECTION: Ne pas déconnecter en mode permanent if self.permanent_connection: self.log_action("🔒 Mode permanent activé - Pas de déconnexion automatique") return self.log_action("🔌 Tentative de déconnexion automatique d'Outlook...") # Libération explicite de la session COM if self.outlook: try: del self.outlook self.outlook = None self.log_action("🧹 Objet Outlook supprimé") except Exception as e: self.log_action(f"⚠️ Erreur lors de la suppression de self.outlook : {e}") try: pythoncom.CoUninitialize() self.log_action("✅ COM libéré avec succès") except Exception as e: self.log_action(f"⚠️ Erreur lors de CoUninitialize : {e}") # Nettoyage mémoire import gc gc.collect() self.log_action("🧠 Garbage collector déclenché") self.outlook_connected = False self.log_action("✅ Outlook déconnecté automatiquement (session COM uniquement)") except Exception as e: self.log_action(f"❌ Erreur dans auto_disconnect : {e}") def reset_counter(self): self.links_clicked = 0 self.total_reward = 0.0 print("Compteurs réinitialisés") def close(self): print("Fermeture de OutlookHandler") if hasattr(self, 'keep_alive_timer'): self.keep_alive_timer.stop() if self.outlook: self.outlook.Quit() pythoncom.CoUninitialize() self.close_browser() self.terminate_qt_processes() print("OutlookHandler fermé") def load_config(self): config_path = r"C:\autolook\config.json" if os.path.exists(config_path): with open(config_path, 'r') as f: return json.load(f) return {} def get_mailboxes(self): """ Récupère la liste des boîtes mail Outlook disponibles """ try: if not self.outlook and not self.connect(): self.log_action("❌ Impossible de se connecter à Outlook") return [] mailboxes = [] for folder in self.outlook.Folders: try: mailbox_name = folder.Name mailboxes.append(mailbox_name) except Exception as e: self.log_action(f"⚠️ Erreur lecture boîte mail: {str(e)}") continue if not mailboxes: self.log_action("⚠️ Aucune boîte mail trouvée") else: self.log_action(f"✅ {len(mailboxes)} boîte(s) mail chargée(s)") return mailboxes except Exception as e: self.log_action(f"❌ Erreur lors de la récupération des boîtes mail : {str(e)}") return [] def find_folder(self, root_folder, folder_name): """Recherche un dossier uniquement dans la boîte de réception et ses sous-dossiers""" try: # Obtenir la boîte de réception inbox = root_folder.Folders["Boîte de réception"] # Si on cherche la boîte de réception elle-même if inbox.Name.strip() == folder_name.strip(): return inbox # Sinon, chercher dans les sous-dossiers return self._find_folder_recursive(inbox, folder_name) except Exception as e: print(f"Erreur lors de la recherche du dossier : {str(e)}") return None def _find_folder_recursive(self, folder, folder_name): """Recherche récursive dans les sous-dossiers""" try: for subfolder in folder.Folders: if subfolder.Name.strip() == folder_name.strip(): return subfolder result = self._find_folder_recursive(subfolder, folder_name) if result: return result return None except Exception as e: print(f"Erreur lors de la recherche récursive : {str(e)}") return None def is_valid_folder(self, folder): """Vérifie si le dossier fait partie de la boîte de réception ou de ses sous-dossiers""" try: if not folder: return False current = folder while current: if current.Name == "Boîte de réception": return True current = current.Parent return False except Exception as e: print(f"Erreur lors de la validation du dossier : {str(e)}") return False def scan_emails(self, mailbox, folder, sender, subject_keywords, link_keywords, max_emails, validity_days=None): """ Scan and process emails with enhanced control for exact email count processing and support for processing in cycles - VERSION OPTIMISÉE """ # === INITIALISATION (IDENTIQUE) === self.should_stop = False # === CONFIGURATION DE LA REDIRECTION DES LOGS (IDENTIQUE) === def log_to_shared_history(message, category='system'): """Fonction pour rediriger tous les logs vers l'historique partagé""" try: if hasattr(self, 'scan_log_history') and hasattr(self, 'current_scan_active') and self.current_scan_active: from PyQt5.QtCore import QDateTime timestamp = QDateTime.currentDateTime().toString("HH:mm:ss") icon = '⚙️' if category == 'email' or '📧' in message: icon = '📧' elif category == 'error' or '❌' in message or 'erreur' in message.lower(): icon = '❌' elif category == 'success' or '✅' in message or 'succès' in message.lower() or 'terminé' in message.lower(): icon = '✅' elif category == 'count' or '🔢' in message or 'compteur' in message.lower() or 'emails traités' in message: icon = '🔢' elif '🔄' in message or 'cycle' in message.lower(): icon = '🔄' elif '📊' in message or 'objectif' in message.lower(): icon = '📊' elif '🏁' in message or 'scan terminé' in message.lower(): icon = '🏁' elif '📡' in message or 'connexion' in message.lower(): icon = '📡' elif '📬' in message or 'boîte mail' in message.lower(): icon = '📬' elif '📁' in message or 'dossier' in message.lower(): icon = '📁' elif '🔃' in message or 'tri' in message.lower(): icon = '🔃' elif '🚀' in message or 'début' in message.lower(): icon = '🚀' elif '⛔' in message or 'arrêté' in message.lower(): icon = '⛔' elif '⏩' in message or 'ignoré' in message.lower(): icon = '⏩' elif '🧠' in message or 'détection' in message.lower(): icon = '🧠' elif 'info:' in message.lower(): icon = 'ℹ️' full_entry = f"{timestamp} {icon} {message}" self.scan_log_history.append(full_entry) print(f"SCAN_LOG_ADDED: {full_entry}") if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'log_action'): if hasattr(self, '_original_log_action') and self._original_log_action: self._original_log_action(message, category) else: self.outlook_widget.log_action(message, category) except Exception as e: print(f"Erreur log_to_shared_history: {e}") if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'log_action'): self.outlook_widget.log_action(message, category) # Sauvegarder la méthode log_action originale et la remplacer self._original_log_action = getattr(self, 'log_action', None) self.log_action = log_to_shared_history # === DÉBUT DU SCAN (IDENTIQUE) === self.log_action("🟢 Clic sur 'Démarrer scan' reçu") if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'save_current_log'): self.outlook_widget.save_current_log() self.reset_counter() if self.duplicates_detection_enabled: self.log_action("🧠 Détection des doublons : activée") else: self.log_action("🧠 Détection des doublons : désactivée") # === VARIABLES D'INITIALISATION (IDENTIQUE) === clicked_links = 0 processed_count = 0 matching_emails = 0 processed_email_ids = set() valid_links_batch = [] emails_processed = set() connection_check_interval = 10 total_processed = 0 doublons_count = 0 self.log_action(f"ℹ️ Scan initié - max_emails={max_emails}, mailbox={mailbox}, folder={folder}") # *** OPTIMISATION 1 : Cache pour les vérifications de connexion *** last_connection_check = 0 connection_check_cache_duration = 30 # secondes if hasattr(self, 'cycle_processing_enabled') and self.cycle_processing_enabled and max_emails: if hasattr(self, 'min_emails_per_cycle') and hasattr(self, 'max_emails_per_cycle'): self.log_action( f"🔄 Traitement par cycle activé: {self.min_emails_per_cycle}-{self.max_emails_per_cycle} emails par cycle, " f"pause de {self.min_pause_between_cycles}-{self.max_pause_between_cycles}s") else: self.log_action(f"🔄 Traitement par cycle activé: {self.emails_per_cycle} emails par cycle, " f"pause de {self.pause_between_cycles}s") try: # === VÉRIFICATION ET SÉCURISATION DE LA CONNEXION OUTLOOK (IDENTIQUE) === try: self.log_action("📡 Connexion à Outlook en cours...") if not self.outlook and not self.connect(): self.log_action("❌ Erreur: Impossible de se connecter à Outlook", 'error') raise Exception("Impossible de se connecter à Outlook") self.check_connection() self.log_action("✅ Connexion Outlook vérifiée", 'success') except Exception as e: self.log_action(f"❌ Erreur de connexion: {str(e)}", 'error') self.reconnect() # === RECHERCHE ET VALIDATION DU DOSSIER (IDENTIQUE) === self.log_action("📬 Recherche de la boîte mail...") root_folder = next((f for f in self.outlook.Folders if f.Name == mailbox), None) if not root_folder: raise Exception(f"Boîte mail '{mailbox}' non trouvée") self.log_action("📁 Recherche du dossier sélectionné...") target_folder = self.find_folder(root_folder, folder) if not target_folder: raise Exception(f"Dossier '{folder}' non trouvé") if not self.is_valid_folder(target_folder): raise Exception(f"Le dossier '{folder}' n'est pas dans la boîte de réception") # === RÉCUPÉRATION ET TRI DES MESSAGES - OPTIMISATION === self.log_action("🔃 Tri des messages par date de réception...") messages = self.safe_rpc_call(lambda: target_folder.Items) self.safe_rpc_call(lambda: messages.Sort("[ReceivedTime]", True)) total_in_folder = self.safe_rpc_call(lambda: messages.Count) if hasattr(self, 'outlook_widget'): self.outlook_widget.email_counter.setText(str(total_in_folder)) self.scan_progress.emit(0) emails_to_process = max_emails if max_emails else total_in_folder if emails_to_process > total_in_folder: emails_to_process = total_in_folder self.log_action( f"📊 Objectif: Traiter exactement {emails_to_process} emails sur {total_in_folder} disponibles") if getattr(self, 'cycle_processing_enabled', False): self.log_action("🔄 Mode de traitement : Cycle (traitement par lots)") elif getattr(self, 'multilink_processing_enabled', False): self.log_action("🌐 Mode de traitement : Multilink (plusieurs liens en parallèle)") else: self.log_action("✅ Mode de traitement : Normal (séquentiel)") # === TRAITEMENT SELON LE MODE === if hasattr(self, 'cycle_processing_enabled') and self.cycle_processing_enabled and max_emails: # TRAITEMENT PAR CYCLE (IDENTIQUE) remaining_emails = emails_to_process cycle_number = 1 self.cycle_stats = [] while remaining_emails > 0 and not self.should_stop: emails_in_cycle = min(remaining_emails, self.get_random_emails_per_cycle()) self.log_action(f"🔄 Cycle {cycle_number}: traitement de {emails_in_cycle} emails") cycle_start_time = datetime.now().strftime('%d/%m/%Y %H:%M:%S') self.cycle_status_update.emit(cycle_number, emails_in_cycle, remaining_emails, 0) processed_in_cycle, clicked_in_cycle, matching_in_cycle = self._process_email_cycle( target_folder, messages, emails_in_cycle, sender, subject_keywords, link_keywords, processed_email_ids, emails_processed, connection_check_interval ) processed_count += processed_in_cycle clicked_links += clicked_in_cycle matching_emails += matching_in_cycle remaining_emails -= processed_in_cycle self.log_action( f"🔄 Fin du cycle {cycle_number}: {processed_in_cycle} emails traités, {clicked_in_cycle} liens cliqués") pause_time = self.get_random_pause_between_cycles() self.log_action(f"⏱️ Pause de {pause_time:.1f} secondes...") cycle_stats = { 'emails_processed': processed_in_cycle, 'pause_between_cycles': pause_time, 'start_time': cycle_start_time, 'end_time': datetime.now().strftime('%d/%m/%Y %H:%M:%S') } self.cycle_stats.append(cycle_stats) if hasattr(self, 'outlook_widget'): self.outlook_widget.cycle_stats = self.cycle_stats self.cycle_status_update.emit(cycle_number, emails_in_cycle, remaining_emails, self.pause_between_cycles) if remaining_emails > 0 and not self.should_stop: pause_time = self.get_random_pause_between_cycles() self.log_action(f"⏱️ Pause de {pause_time:.1f} secondes...") progress = int(((emails_to_process - remaining_emails) / emails_to_process) * 100) self.scan_progress.emit(progress) if hasattr(self, 'outlook_widget'): self.outlook_widget.email_counter.setText(f"{remaining_emails} / {emails_to_process}") if hasattr(self.outlook_widget, 'display_temporary_message'): self.outlook_widget.display_temporary_message( f"Pause entre cycles: {pause_time:.1f} secondes avant le cycle {cycle_number + 1}" ) pause_start = time.time() while time.time() - pause_start < pause_time and not self.should_stop: time.sleep(0.1) QApplication.processEvents() cycle_number += 1 if self.should_stop: # Vérification de suspension système if hasattr(self.outlook_widget, '_threads_suspended') and self.outlook_widget._threads_suspended: while self.outlook_widget._threads_suspended and not self.should_stop: time.sleep(0.01) # Attente passive # Ne pas utiliser QApplication.processEvents() ici if self.should_stop: break self.log_action("⛔ Traitement arrêté pendant la pause entre cycles") break elif hasattr(self, 'multilink_processing_enabled') and self.multilink_processing_enabled: # TRAITEMENT MULTILINK (IDENTIQUE) self.log_action("🌐 Mode de traitement: Traitement multilink") processed_count, clicked_links, matching_emails = self._process_email_multilink( target_folder, messages, max_emails, sender, subject_keywords, link_keywords, processed_email_ids, emails_processed, connection_check_interval ) else: # === TRAITEMENT STANDARD OPTIMISÉ === self.log_action("🚀 Début du traitement des emails...") # *** OPTIMISATION 2 : Traitement par batches au lieu d'une conversion complète *** batch_size = min(50, emails_to_process) # Traiter par batches de 50 max message_index = 1 # Les collections Outlook commencent à 1 # *** OPTIMISATION 3 : Cache des mots-clés pour éviter les calculs répétitifs *** keyword_cache = set(keyword.lower() for keyword in link_keywords) while processed_count < emails_to_process and not self.should_stop: # *** OPTIMISATION 4 : Vérification de connexion avec cache *** import time current_time = time.time() if current_time - last_connection_check > connection_check_cache_duration: self.check_connection() last_connection_check = current_time # Gestion de la pause if self.is_paused: while self.is_paused and not self.should_stop: time.sleep(0.1) QApplication.processEvents() if self.should_stop: # Vérification de suspension système if hasattr(self.outlook_widget, '_threads_suspended') and self.outlook_widget._threads_suspended: while self.outlook_widget._threads_suspended and not self.should_stop: time.sleep(0.01) # Attente passive # Ne pas utiliser QApplication.processEvents() ici if self.should_stop: break self.log_action("⛔ Traitement arrêté") break try: # *** OPTIMISATION 5 : Accès direct par index au lieu de conversion en liste *** message = messages.Item(message_index) message_index += 1 email_id = message.EntryID if email_id in processed_email_ids or email_id in emails_processed: continue total_processed += 1 processed_email_ids.add(email_id) processed_count += 1 # MISE À JOUR DE LA PROGRESSION progress = int((processed_count / emails_to_process) * 100) self.scan_progress.emit(progress) if hasattr(self, 'outlook_widget'): remaining = emails_to_process - processed_count self.outlook_widget.email_counter.setText(f"{remaining} / {emails_to_process}") QApplication.processEvents() self.log_action(f"ℹ️ Email {processed_count}/{emails_to_process} - ID: {email_id[:8]}...") # *** OPTIMISATION 6 : Vérification rapide des critères AVANT analyse des liens *** if self.should_process_message(message, sender, subject_keywords): sender_email = getattr(message, 'SenderEmailAddress', "Adresse inconnue") subject = getattr(message, 'Subject', "Sujet inconnu") # *** OPTIMISATION 7 : Fonction find_valid_link optimisée *** link = self._find_valid_link_optimized(message, keyword_cache) if link: # Identifier les mots-clés trouvés dans le lien keywords_found = [] link_lower = link.lower() for keyword in link_keywords: if keyword.lower() in link_lower: keywords_found.append(keyword) email_data = { 'sender': sender_email, 'subject': subject, 'link': link, 'keywords_found': keywords_found, 'action': 'en_cours' } if hasattr(self, 'outlook_widget'): self.outlook_widget.log_email_details(email_data) # Vérifier les doublons if self.duplicates_detection_enabled and self.links_handler.is_link_processed(link): email_data['action'] = 'doublon_ignoré' if hasattr(self, 'outlook_widget'): self.outlook_widget.log_email_details(email_data) doublons_count += 1 self.log_action(f"🔍 Doublon détecté et ignoré: {link[:50]}...") continue matching_emails += 1 email_data['action'] = 'cliqué' if hasattr(self, 'outlook_widget'): self.outlook_widget.log_email_details(email_data) self.log_action(f"📧 Email valide trouvé: {subject[:40]}..." if len( subject) > 40 else f"📧 Email valide: {subject}", 'email') if hasattr(self, 'outlook_widget'): self.outlook_widget.record_action( source=sender_email, link=link, message=message ) valid_links_batch.append({ 'sender': sender_email, 'link': link, 'message': message, 'subject': subject }) emails_processed.add(email_id) # Mettre à jour la bannière if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser if getattr(self, 'banner_position', 'bottom') != "disabled" and hasattr(browser, 'bottom_banner'): title = message.Subject if hasattr(message, 'Subject') else "Sujet inconnu" if hasattr(browser.bottom_banner, 'show_banner'): browser.bottom_banner.show_banner(f"Onglet 1 / {title}") elif hasattr(browser.bottom_banner, 'setText'): browser.bottom_banner.setText(f"Onglet 1 / {title}") # *** OPTIMISATION 8 : Traitement en batch plus efficace *** batch_limit = getattr(self, 'links_count', 1) if len(valid_links_batch) >= batch_limit: self.log_action(f"🔗 Traitement d'un lot de {len(valid_links_batch)} liens...") clicked_this_batch = self.process_batch(valid_links_batch) clicked_links += clicked_this_batch self.log_action(f"✅ Lot traité: {clicked_this_batch} liens cliqués", 'success') valid_links_batch = [] except Exception as e: error_str = str(e) if "-2147352567" in error_str or "déplacé ou supprimé" in error_str or "moved or deleted" in error_str: self.log_action(f"⏩ Email ignoré (supprimé/déplacé pendant le traitement)") continue elif "RPC" in error_str: self.log_action(f"❌ Erreur RPC lors du traitement d'un email: {error_str}", 'error') self.reconnect() last_connection_check = 0 # Force reconnection check continue else: self.log_action(f"❌ Erreur lors du traitement d'un email: {error_str}", 'error') # === TRAITEMENT DES LIENS RESTANTS (IDENTIQUE) === if valid_links_batch: self.log_action(f"🔗 Traitement du lot final de {len(valid_links_batch)} liens...") clicked_this_batch = self.process_batch(valid_links_batch) clicked_links += clicked_this_batch self.log_action(f"✅ Lot final traité: {clicked_this_batch} liens cliqués", 'success') self.log_action(f"🏁 Scan terminé: {processed_count}/{emails_to_process} emails traités", 'success') except Exception as e: self.log_action(f"❌ Erreur lors du traitement: {str(e)}", 'error') raise finally: # === RESTAURATION ET NETTOYAGE (IDENTIQUE) === if hasattr(self, '_original_log_action') and self._original_log_action: self.log_action = self._original_log_action total_entries = len(getattr(self, 'scan_log_history', [])) print(f"DEBUG: scan_log_history final contient {total_entries} entrées") if hasattr(self, 'outlook_widget'): try: messages = self.safe_rpc_call(lambda: target_folder.Items) self.outlook_widget.email_counter.setText(str(self.safe_rpc_call(lambda: messages.Count))) if hasattr(self, 'total_reward'): self.outlook_widget.reward_display.setText(str(int(self.total_reward))) if hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser if hasattr(browser, 'bottom_banner'): browser.bottom_banner.hide_banner() valid_emails = matching_emails total_emails_processed = processed_count skipped_emails = getattr(self, 'links_skipped', 0) current_time = datetime.now().strftime("%H:%M:%S") summary_text = ( f"{current_time} - ✅ Emails traités: {total_emails_processed}/{emails_to_process}\n" f"{current_time} - 🏁 Scan terminé\n" f"{current_time} - 📧 • Emails valides: {valid_emails}\n" f"{current_time} - ℹ️ • Total traité: {total_emails_processed}\n" f"{current_time} - 🔗 • Liens cliqués: {clicked_links}\n" f"{current_time} - 🔍 • Doublons détectés: {doublons_count}\n" f"{current_time} - 📧 • Emails ignorés: {skipped_emails}" ) if hasattr(self, 'cycle_processing_enabled') and self.cycle_processing_enabled and max_emails: summary_text += f"\n{current_time} - 🔄 • Mode cycle: {getattr(self, 'emails_per_cycle', 'N/A')} emails / {getattr(self, 'pause_between_cycles', 'N/A')}s pause" if hasattr(self.outlook_widget, 'display_temporary_summary'): self.outlook_widget.display_temporary_summary(summary_text, 15) else: self.log_action("🏁 Scan terminé", 'success') self.log_action(f" • Emails valides: {valid_emails}", 'email') self.log_action(f" • Total traité: {total_emails_processed}", 'email') self.log_action(f" • Liens cliqués: {clicked_links}", 'success') self.log_action(f" • Doublons détectés: {doublons_count}", 'system') self.log_action(f" • Emails ignorés: {skipped_emails}", 'system') if hasattr(self.outlook_widget, 'finish_scan'): self.outlook_widget.finish_scan() if hasattr(self, 'outlook_widget') and self.outlook_widget: if hasattr(self.outlook_widget, 'lock_session_checkbox') and self.outlook_widget.lock_session_checkbox.isChecked(): self.outlook_widget.log_action("🔒 Lancement du verrouillage de session...") QTimer.singleShot(1000, self.outlook_widget.lock_windows_session) if hasattr(self.outlook_widget, 'shutdown_pc_checkbox') and self.outlook_widget.shutdown_pc_checkbox.isChecked(): self.outlook_widget.log_action("🔌 Programmation de l'extinction du PC...") QTimer.singleShot(1000, self.outlook_widget.shutdown_windows) QTimer.singleShot(8000, lambda: self.reset_counters_after_scan()) except Exception as e: self.log_action(f"❌ Erreur lors de la mise à jour finale du compteur: {str(e)}", 'error') if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget.main_window, 'report_toggle') and \ self.outlook_widget.main_window.report_toggle.isChecked(): self.outlook_widget.display_report() self.disable_report_toggle() return clicked_links, matching_emails def scan_emails_pattern(self, mailbox_name, folder_names, sender, subject_keywords, link_keywords, max_emails): """ Scan des emails en mode pattern (analyse du contexte du corps du mail). """ import gc import pythoncom self.should_stop = False self.reset_counter() clicked_links = 0 matching_emails = 0 processed_email_ids = set() connection_check_interval = 10 processed_count = 0 self.log_action(f"🚀 Scan pattern démarré - max_emails={max_emails}, mailbox={mailbox_name}") self.log_action(f"📁 Dossiers à traiter : {folder_names}") try: if not self.outlook and not self.connect(): raise Exception("Impossible de se connecter à Outlook") self.check_connection() root_folder = next((f for f in self.outlook.Folders if f.Name == mailbox_name), None) if not root_folder: raise Exception(f"Boîte mail '{mailbox_name}' non trouvée") # Traiter chaque dossier sélectionné for folder_name in folder_names: if self.should_stop: # Vérification de suspension système if hasattr(self.outlook_widget, '_threads_suspended') and self.outlook_widget._threads_suspended: while self.outlook_widget._threads_suspended and not self.should_stop: time.sleep(0.01) # Attente passive # Ne pas utiliser QApplication.processEvents() ici if self.should_stop: break self.log_action("⛔ Traitement arrêté") break self.log_action(f"📂 Traitement du dossier : {folder_name}") target_folder = self.find_folder(root_folder, folder_name) if not target_folder: self.log_action(f"⚠️ Dossier '{folder_name}' non trouvé, ignoré") continue if not self.is_valid_folder(target_folder): self.log_action(f"⚠️ Le dossier '{folder_name}' n'est pas dans la boîte de réception, ignoré") continue messages = self.safe_rpc_call(lambda: target_folder.Items) self.safe_rpc_call(lambda: messages.Sort("[ReceivedTime]", True)) total_in_folder = self.safe_rpc_call(lambda: messages.Count) self.log_action(f"🔎 {total_in_folder} messages trouvés dans {folder_name}") if total_in_folder == 0: self.log_action(f"📭 Aucun message dans le dossier {folder_name}") continue # Calculer le nombre d'emails à traiter dans ce dossier remaining_emails = (max_emails - processed_count) if max_emails else total_in_folder emails_to_process_folder = min(remaining_emails, total_in_folder) messages_list = list(messages) for idx, message in enumerate(messages_list): if self.should_stop: # Vérification de suspension système if hasattr(self.outlook_widget, '_threads_suspended') and self.outlook_widget._threads_suspended: while self.outlook_widget._threads_suspended and not self.should_stop: time.sleep(0.01) # Attente passive # Ne pas utiliser QApplication.processEvents() ici if self.should_stop: break self.log_action("⛔ Traitement arrêté") break if max_emails and processed_count >= max_emails: self.log_action(f"⏹️ Limite de {max_emails} emails atteinte") break try: email_id = str(getattr(message, 'EntryID', 'ID inconnu')) except Exception as e: self.log_action(f"⚠ Erreur lecture EntryID : {e}") continue if email_id in processed_email_ids: continue processed_email_ids.add(email_id) processed_count += 1 try: subject = str(getattr(message, 'Subject', 'Sujet inconnu')) except Exception as e: subject = "Sujet illisible" self.log_action(f"⚠ Erreur lecture Subject : {e}") try: sender_email = str(getattr(message, 'SenderEmailAddress', 'Inconnu')) except Exception as e: sender_email = "Inconnu" self.log_action(f"⚠ Erreur lecture SenderEmailAddress : {e}") self.log_action(f"📧 DEBUG MAIL #{processed_count} (dossier: {folder_name})") self.log_action(f" • EntryID : {email_id}") self.log_action(f" • Sujet : {subject}") self.log_action(f" • Expéditeur : {sender_email}") try: if self.should_process_message(message, sender, subject_keywords): self.log_action(f"🔍 Tentative traitement (mode=pattern)") # Utiliser find_valid_link avec mode="pattern" link = self.find_valid_link(message, link_keywords, mode="pattern") if link: self.log_action(f"🔗 Lien trouvé (pattern) : {link}") self.links_handler.add_processed_link(link, {'processed_date': datetime.now().isoformat()}) matching_emails += 1 clicked_links += 1 if hasattr(self, 'outlook_widget'): self.outlook_widget.record_action(sender_email, link, None) # Charger le lien dans le mini navigateur if hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser current_tab = browser.get_current_tab() if current_tab and current_tab.web_view: current_tab.web_view.load(QUrl(link)) browser.update_url(QUrl(link)) self.log_action(f"🌐 Lien chargé : {link}") # Attente simulée pour affichage pause_time = self.get_random_pause_time() self.log_action(f"⏱️ Pause après clic : {pause_time:.1f} secondes") loop = QEventLoop() QTimer.singleShot(int(pause_time * 1000), loop.quit) loop.exec_() # Suppression si activée if self.auto_delete: try: message.Delete() self.log_action(f"🗑️ Email supprimé : {subject}") except Exception as e: self.log_action(f"⚠️ Erreur suppression : {e}") else: self.log_action("⛔ Aucun lien valide trouvé (pattern)") except Exception as e: self.log_action(f"❌ Erreur lors du traitement email ID {email_id} : {e}") import traceback self.log_action(f"TRACE: {traceback.format_exc()}") continue # Nettoyage try: pythoncom.CoFreeUnusedLibraries() gc.collect() except Exception as e: self.log_action(f"⚠ Erreur lors du nettoyage mémoire : {e}") # Vérification périodique de la connexion if processed_count % connection_check_interval == 0: try: self.check_connection() except Exception as e: self.log_action(f"⚠ Problème de connexion détecté : {e}") if emails_to_process_folder and processed_count >= emails_to_process_folder: break except Exception as e: self.log_action(f"❌ Erreur globale lors du scan pattern : {e}") import traceback logger.error(f"Erreur détaillée : {traceback.format_exc()}") raise finally: if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'finish_scan'): self.outlook_widget.finish_scan() self.log_action(f"🏁 Scan pattern terminé : {processed_count} emails traités sur {len(folder_names)} dossier(s)") self.log_action(f"📊 Résultats : {clicked_links} liens cliqués, {matching_emails} emails correspondants") return clicked_links, matching_emails def _process_email_cycle(self, target_folder, messages, emails_in_cycle, sender, subject_keywords, link_keywords, processed_email_ids, emails_processed, connection_check_interval): """ Traite un lot d'emails dans un cycle, en utilisant une valeur aléatoire dans la plage configurée Args: target_folder: Dossier Outlook à analyser messages: Liste des messages disponibles emails_in_cycle: Nombre maximum d'emails à traiter dans ce cycle sender, subject_keywords, link_keywords: Critères de filtrage processed_email_ids, emails_processed: Ensembles pour suivre les emails déjà traités connection_check_interval: Intervalle pour vérifier la connexion Returns: Tuple (emails_processed, links_clicked, matching_emails) """ # Initialisation des compteurs processed_count = 0 clicked_links = 0 matching_emails = 0 total_processed = 0 valid_links_batch = [] doublons_count = 0 try: # Déterminer le nombre réel d'emails à traiter pour ce cycle de manière aléatoire target_emails = min(emails_in_cycle, self.get_random_emails_per_cycle()) self.log_action( f"🔄 Ce cycle traitera exactement {target_emails} emails (sur {emails_in_cycle} disponibles)") # Obtenir une liste des messages messages_list = list(messages) message_index = 0 while processed_count < target_emails and message_index < len(messages_list): # Vérification périodique de la connexion if total_processed % connection_check_interval == 0: self.check_connection() # Gestion de la pause if self.is_paused: while self.is_paused and not self.should_stop: time.sleep(0.1) QApplication.processEvents() # Vérification de l'arrêt demandé if self.should_stop: # Vérification de suspension système if hasattr(self.outlook_widget, '_threads_suspended') and self.outlook_widget._threads_suspended: while self.outlook_widget._threads_suspended and not self.should_stop: time.sleep(0.01) # Attente passive # Ne pas utiliser QApplication.processEvents() ici if self.should_stop: break self.log_action("⛔ Traitement du cycle arrêté") break # Obtenir le message actuel message = messages_list[message_index] message_index += 1 # Vérifier si l'email a déjà été traité email_id = message.EntryID if email_id in processed_email_ids or email_id in emails_processed: continue # Incrémenter le compteur d'emails traités total_processed += 1 processed_email_ids.add(email_id) processed_count += 1 try: # Vérifier si le message correspond aux critères if self.should_process_message(message, sender, subject_keywords): sender_email = getattr(message, 'SenderEmailAddress', "Adresse inconnue") self.log_action(f"\n📨 Traitement email de: {sender_email}") self.log_action(f"📝 Sujet: {message.Subject}") # Chercher un lien valide link = self.find_valid_link(message, link_keywords) if link: # Vérifier si le lien a déjà été traité if self.duplicates_detection_enabled and self.links_handler.is_link_processed(link): self.log_action(f"🔶 Doublon détecté: {link}") doublons_count += 1 continue # Incrémenter le compteur d'emails correspondants matching_emails += 1 self.log_action(f"✅ Email correspondant #{matching_emails}/{target_emails}") # Enregistrer l'action si le widget est disponible if hasattr(self, 'outlook_widget'): self.outlook_widget.record_action( source=sender_email, link=link, message=message ) # Ajouter le lien au lot à traiter valid_links_batch.append({ 'sender': sender_email, 'link': link, 'message': message, 'subject': message.Subject }) self.log_action(f"🔗 Lien valide trouvé: {link}") if hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser if hasattr(browser, 'bottom_banner'): title = message.Subject if hasattr(message, 'Subject') else "Sujet inconnu" browser.bottom_banner.show_banner(f"Onglet 1 / {title}") emails_processed.add(email_id) # Traiter le lot si assez d'emails sont collectés if len(valid_links_batch) >= self.links_count: clicked_this_batch = self.process_batch(valid_links_batch) clicked_links += clicked_this_batch valid_links_batch = [] # Pause aléatoire entre les lots si ce n'est pas le dernier if processed_count < target_emails: pause_time = self.get_random_between_pages_time() self.log_action(f"⏱️ Pause de {pause_time:.1f} secondes entre les lots") time.sleep(pause_time) except Exception as e: if "RPC" in str(e): self.log_action(f"❌ Erreur RPC lors du traitement d'un email: {str(e)}") self.reconnect() continue self.log_action(f"❌ Erreur lors du traitement d'un email: {str(e)}") # Mettre à jour la progression if hasattr(self, 'scan_progress'): progress = int((processed_count / target_emails) * 100) self.scan_progress.emit(progress) QApplication.processEvents() # Traiter les liens restants if valid_links_batch: clicked_this_batch = self.process_batch(valid_links_batch) clicked_links += clicked_this_batch # Mettre à jour le compteur de doublons self.doublons_count = getattr(self, 'doublons_count', 0) + doublons_count return processed_count, clicked_links, matching_emails except Exception as e: self.log_action(f"❌ Erreur dans le cycle de traitement: {str(e)}") return processed_count, clicked_links, matching_emails def _process_email_multilink(self, target_folder, messages, max_emails, sender, subject_keywords, link_keywords, processed_email_ids, emails_processed, connection_check_interval): """ Traite les emails en mode multilink, en traitant plusieurs liens simultanément selon le nombre configuré dans links_count. """ # Initialisation des compteurs total_processed = 0 clicked_links = 0 matching_emails = 0 doublons_count = 0 try: # Obtenir une liste des messages messages_list = list(messages) message_index = 0 # Déterminer le nombre maximum de liens à traiter simultanément max_links = getattr(self, 'links_count', 1) self.log_action(f"🌐 Mode multilink activé - Traitement de {max_links} liens simultanément") while total_processed < max_emails and message_index < len(messages_list): # Vérification périodique de la connexion if total_processed % connection_check_interval == 0: self.check_connection() # Gestion de la pause if self.is_paused: while self.is_paused and not self.should_stop: time.sleep(0.1) QApplication.processEvents() # Vérification de l'arrêt demandé if self.should_stop: # Vérification de suspension système if hasattr(self.outlook_widget, '_threads_suspended') and self.outlook_widget._threads_suspended: while self.outlook_widget._threads_suspended and not self.should_stop: time.sleep(0.01) # Attente passive # Ne pas utiliser QApplication.processEvents() ici if self.should_stop: break self.log_action("⛔ Traitement multilink arrêté") break # Collecter un lot de liens à traiter en parallèle valid_links_batch = [] collected = 0 while collected < max_links and message_index < len(messages_list) and total_processed < max_emails: # Obtenir le message actuel message = messages_list[message_index] message_index += 1 # Vérifier si l'email a déjà été traité email_id = message.EntryID if email_id in processed_email_ids or email_id in emails_processed: continue # Incrémenter le compteur d'emails traités total_processed += 1 processed_email_ids.add(email_id) try: # Vérifier si le message correspond aux critères if self.should_process_message(message, sender, subject_keywords): sender_email = getattr(message, 'SenderEmailAddress', "Adresse inconnue") self.log_action(f"📨 Traitement email de: {sender_email}") self.log_action(f"📝 Sujet: {message.Subject}") # Chercher un lien valide link = self.find_valid_link(message, link_keywords) if link: # Vérifier si le lien a déjà été traité if (self.duplicates_detection_enabled and hasattr(self, 'links_handler') and self.links_handler.is_link_processed(link)): self.log_action(f"🔶 Doublon détecté: {link}") doublons_count += 1 continue # Incrémenter le compteur d'emails correspondants matching_emails += 1 self.log_action(f"✅ Email correspondant #{matching_emails}") # Ajouter le lien au lot à traiter valid_links_batch.append({ 'sender': sender_email, 'link': link, 'message': message, 'subject': message.Subject }) collected += 1 self.log_action(f"🔗 Lien valide {collected}/{max_links} ajouté au lot") if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser if hasattr(browser, 'bottom_banner') and hasattr(browser, 'build_banner_text'): title = message.Subject if hasattr(message, 'Subject') else "Sans sujet" banner_text = browser.build_banner_text(title) browser.bottom_banner.show_banner(banner_text) emails_processed.add(email_id) except Exception as e: if "RPC" in str(e): self.log_action(f"❌ Erreur RPC lors du traitement d'un email: {str(e)}") self.reconnect() continue self.log_action(f"❌ Erreur lors du traitement d'un email: {str(e)}") # Traiter le lot si des liens ont été collectés if valid_links_batch: self.log_action(f"🔄 Traitement d'un lot de {len(valid_links_batch)} liens simultanément") clicked_this_batch = self.process_batch(valid_links_batch) clicked_links += clicked_this_batch # Mise à jour de la progression if hasattr(self, 'scan_progress'): progress = int((total_processed / max_emails) * 100) self.scan_progress.emit(progress) # Mise à jour du compteur d'emails if hasattr(self, 'outlook_widget'): remaining = max_emails - total_processed self.outlook_widget.email_counter.setText(f"{remaining} / {max_emails}") QApplication.processEvents() if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): banner = self.outlook_widget.mini_browser.bottom_banner if banner: banner.hide_banner() QApplication.processEvents() # Mettre à jour le compteur de doublons self.doublons_count = getattr(self, 'doublons_count', 0) + doublons_count # Assurer que tous les onglets supplémentaires sont fermés à la fin if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): # Nettoyage des onglets dans les deux navigateurs (mini et principal) try: # Nettoyage du mini navigateur if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): mini_browser = self.outlook_widget.mini_browser mini_tab_count = mini_browser.tab_widget.count() if mini_tab_count > 1: self.log_action(f"Nettoyage du mini navigateur: fermeture de {mini_tab_count - 1} onglets") # Sélectionner d'abord le premier onglet mini_browser.tab_widget.setCurrentIndex(0) QApplication.processEvents() # Fermer les autres onglets for i in range(mini_tab_count - 1, 0, -1): try: mini_browser.close_tab(i) QApplication.processEvents() except Exception as e: self.log_action(f"Erreur lors de la fermeture du mini onglet {i}: {str(e)}") # Nettoyage du navigateur principal if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'browser_window'): browser_window = self.outlook_widget.browser_window if hasattr(browser_window, 'tab_widget'): main_tab_count = browser_window.tab_widget.count() if main_tab_count > 1: self.log_action( f"Nettoyage du navigateur principal: fermeture de {main_tab_count - 1} onglets") # Sélectionner d'abord le premier onglet browser_window.tab_widget.setCurrentIndex(0) QApplication.processEvents() # Fermer les autres onglets for i in range(main_tab_count - 1, 0, -1): try: browser_window.close_tab(i) QApplication.processEvents() except Exception as e: self.log_action( f"Erreur lors de la fermeture de l'onglet principal {i}: {str(e)}") try: browser_window.tab_widget.removeTab(i) QApplication.processEvents() except Exception as alt_e: self.log_action(f"Échec de la méthode alternative: {str(alt_e)}") except Exception as e: self.log_action(f"❌ Erreur lors du nettoyage final des onglets: {str(e)}") # Vérification finale final_tab_count = self.outlook_widget.mini_browser.tab_widget.count() if final_tab_count == 1: self.log_action("✅ Un seul onglet conservé") else: self.log_action(f"⚠️ Attention: {final_tab_count} onglet(s) restants après nettoyage") return total_processed, clicked_links, matching_emails except Exception as e: self.log_action(f"❌ Erreur dans le traitement multilink: {str(e)}") # Nettoyage des onglets en cas d'erreur if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): try: tab_count = self.outlook_widget.mini_browser.tab_widget.count() if tab_count > 1: for i in range(tab_count - 1, 0, -1): try: self.outlook_widget.mini_browser.close_tab(i) except: pass except: pass return total_processed, clicked_links, matching_emails def _process_multilink_batch(self, batch): """ Traite un lot de liens en réutilisant les onglets existants Args: batch: Liste de dictionnaires contenant les informations des liens Returns: Nombre de liens cliqués """ clicked_links = 0 try: # Générer un seul temps pour tout le lot display_time = self.get_random_pause_time() between_time = self.get_random_between_pages_time() # Stocker ces valeurs pour que TimerWidget puisse les récupérer self.display_time = display_time self.between_time = between_time if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): # Si le widget a un TimerWidget, mettre à jour directement pour le temps d'affichage if hasattr(self.outlook_widget, 'timer_widget'): self.outlook_widget.timer_widget.set_text(f"Display {display_time:.1f}s", "display") # Chargement des URLs dans les onglets existants for i, link_data in enumerate(batch): # S'assurer que l'onglet i existe if i >= self.outlook_widget.mini_browser.tab_widget.count(): # Cela ne devrait pas arriver si la préparation a été correcte self.log_action(f"⚠️ Création d'un onglet supplémentaire ({i + 1})") self.outlook_widget.mini_browser.add_tab() # Passer à l'onglet i self.outlook_widget.mini_browser.tab_widget.setCurrentIndex(i) current_tab = self.outlook_widget.mini_browser.get_current_tab() # Charger l'URL dans l'onglet actuel if current_tab and current_tab.web_view: current_tab.web_view.load(QUrl(link_data['link'])) self.log_action(f"🌐 URL chargée dans l'onglet {i + 1}: {link_data['link']}") # Vérifier et enregistrer dans l'historique if isinstance(self.outlook_widget.mini_browser, BrowserWindow): self.outlook_widget.mini_browser.update_url(QUrl(link_data['link'])) # Enregistrer le lien comme traité if self.duplicates_detection_enabled: self.links_handler.add_processed_link( link_data['link'], metadata={ 'sender': link_data['sender'], 'subject': link_data['subject'], 'processed_date': datetime.now().isoformat() } ) clicked_links += 1 self.update_links_clicked() # Revenir au premier onglet pour l'affichage self.outlook_widget.mini_browser.tab_widget.setCurrentIndex(0) # Attente unique pour tout le lot self.log_action(f"⏱️ Affichage des {len(batch)} onglets pendant {display_time:.1f} secondes") loop = QEventLoop() QTimer.singleShot(int(display_time * 1000), loop.quit) loop.exec_() # Traitement post-affichage pour tous les messages for link_data in batch: if self.auto_delete: try: link_data['message'].Delete() self.log_action(f"🗑️ Email supprimé : {link_data['subject']}") except Exception as e: self.log_action(f"⚠️ Erreur lors de la suppression : {str(e)}") # Mettre à jour l'affichage du temps de pause dans TimerWidget if hasattr(self.outlook_widget, 'timer_widget'): self.outlook_widget.timer_widget.set_text(f"Pause {between_time:.1f}s", "pause") # Mettre à jour l'affichage du temps de pause dans console_header if hasattr(self.outlook_widget.mini_browser, 'console_header'): self.outlook_widget.mini_browser.console_header.update_page_time(f"Pause {between_time:.1f}s") # Attente unique après le traitement du lot self.log_action(f"⏱️ Pause de {between_time:.1f} secondes avant le prochain lot") loop = QEventLoop() QTimer.singleShot(int(between_time * 1000), loop.quit) loop.exec_() return clicked_links except Exception as e: self.log_action(f"❌ Erreur lors du traitement du lot multilink : {str(e)}") import traceback self.log_action(traceback.format_exc()) return clicked_links def _process_email_batch(self, folder_obj, sender, subject_keywords, link_keywords, max_emails): """ Traite un lot d'emails et retourne les résultats Args: folder_obj: Objet dossier Outlook sender: Filtre d'expéditeur subject_keywords: Liste de mots-clés pour le sujet link_keywords: Liste de mots-clés pour les liens max_emails: Nombre maximum d'emails à traiter dans ce lot Returns: Tuple (nombre de liens cliqués, nombre d'emails correspondants) """ links_clicked = 0 matching_emails = 0 try: # Ici, insérer le code original de traitement des emails # par exemple: emails = folder_obj.Items emails.Sort("[ReceivedTime]", True) # Trier par date de réception (plus récent d'abord) # Traiter les emails processed_emails = 0 for email in emails: if max_emails and processed_emails >= max_emails: break # Vérifier l'expéditeur si spécifié if sender and not self._check_sender(email, sender): continue # Vérifier les mots-clés du sujet si spécifiés if subject_keywords and not self._check_subject(email, subject_keywords): continue # L'email correspond aux critères, extraire et traiter les liens email_links = self._extract_links(email) matched_links = self._filter_links(email_links, link_keywords) # Si des liens correspondent, les traiter if matched_links: matching_emails += 1 # Cliquer sur les liens correspondants for link in matched_links: # Cliquer sur le lien if self._click_link(link, email.Subject): links_clicked += 1 self.links_clicked += 1 else: self.links_skipped += 1 # Marquer l'email comme lu ou le déplacer si nécessaire if hasattr(self, 'mark_as_read') and self.mark_as_read: email.UnRead = False # Supprimer l'email si configuré if hasattr(self, 'delete_after_processing') and self.delete_after_processing: if hasattr(self, 'permanent_delete_enabled') and self.permanent_delete_enabled: email.Delete() # Suppression permanente else: # Déplacer vers la corbeille email.Delete() processed_emails += 1 # Émettre un signal de progression si disponible if hasattr(self, 'scan_progress'): progress = int((processed_emails / max_emails) * 100) if max_emails else 0 self.scan_progress.emit(min(progress, 99)) # Ne jamais atteindre 100% pendant le traitement return links_clicked, matching_emails except Exception as e: logger.error(f"Erreur lors du traitement du lot d'emails: {str(e)}") import traceback logger.error(traceback.format_exc()) return links_clicked, matching_emails def set_multilink_processing(self, enabled): """ Active ou désactive le traitement multilink Args: enabled: True pour activer, False pour désactiver """ self.multilink_processing_enabled = enabled # Si multilink est désactivé, s'assurer que links_count est à 1 if not enabled: self.links_count = 1 self.log_action("Mode standard: traitement d'un lien à la fois") else: # Restaurer la valeur précédente ou défaut si non définie if not hasattr(self, 'links_count') or self.links_count < 1: self.links_count = 1 self.log_action(f"Mode multilink activé: {self.links_count} liens simultanés") # Si le traitement multilink est activé, désactiver le traitement par cycle if enabled and hasattr(self, 'cycle_processing_enabled') and self.cycle_processing_enabled: self.cycle_processing_enabled = False self.log_action("Traitement par cycle désactivé car traitement multilink activé") def set_links_count(self, count): """ Définit le nombre de liens à traiter simultanément en mode multilink Args: count: Nombre de liens """ self.links_count = count self.log_action(f"Nombre de liens en traitement multilink: {count}") def reset_counters_after_scan(self): """Réinitialise les compteurs après un scan terminé""" try: if hasattr(self, 'outlook_widget'): # Réinitialiser les compteurs d'affichage self.outlook_widget.email_counter.setText("---") self.outlook_widget.reward_display.setText("---") self.outlook_widget.time_display.setText("--:--") # Log de confirmation self.log_action("✅ Compteurs réinitialisés") except Exception as e: self.log_action(f"❌ Erreur lors de la réinitialisation des compteurs: {str(e)}") def terminate_qt_processes(self): for proc in psutil.process_iter(['pid', 'name']): if proc.info['name'] == 'QtWebEngineProcess.exe': try: proc.terminate() proc.wait(timeout=5) except psutil.NoSuchProcess: pass except psutil.TimeoutExpired: proc.kill() self.log_action("Processus QtWebEngine terminés") def set_between_pages_time_range(self, min_time, max_time): """Définit la plage de temps de pause entre les pages""" self.min_between_pages_time = min_time self.max_between_pages_time = max_time # Mise à jour de between_time pour que TimerWidget soit en synchronisation self.between_time = (min_time + max_time) / 2 # Mettre à jour le TimerWidget si disponible et en mode pause if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'timer_widget'): if hasattr(self.outlook_widget.timer_widget, 'current_mode') and self.outlook_widget.timer_widget.current_mode == "pause": self.outlook_widget.timer_widget.set_text(f"Pause {self.between_time:.1f}s", "pause") logger.info(f"Plage de temps entre pages définie: {min_time}-{max_time} secondes") def get_random_between_pages_time(self): """Retourne un temps de pause aléatoire pour l'intervalle entre les pages""" import random pause_time = random.uniform(self.min_between_pages_time, self.max_between_pages_time) logger.info(f"Temps de pause entre pages généré: {pause_time:.2f} secondes") return pause_time def process_batch(self, batch): """ Traite un lot de messages en parallèle avec plusieurs onglets et logs détaillés VERSION OPTIMISÉE AVEC FERMETURE DES APPLICATIONS C++ """ clicked_links = 0 created_tabs = [] deleted_count = 0 error_count = 0 try: # Log du début du lot avec détails if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"🔄 " + "═" * 15 + f" DÉBUT LOT DE {len(batch)} EMAILS " + "═" * 15) # *** OPTIMISATION 1 : Calcul unique des temps pour tout le lot *** display_time = self.get_random_pause_time() between_time = self.get_random_between_pages_time() # Stocker ces valeurs pour que TimerWidget puisse les récupérer self.display_time = display_time self.between_time = between_time # *** OPTIMISATION 2 : Vérification COM préventive avec cache *** def check_com_health(): try: if self.outlook: _ = self.outlook.GetDefaultFolder(6).Items.Count return True except: return False if not self.safe_com_operation(check_com_health): self.outlook_widget.log_action("⚠️ Problème COM détecté, tentative de reconnexion...") self.ensure_connected() if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action( f"⏱️ Temps configurés - Affichage: {display_time:.1f}s, Pause: {between_time:.1f}s") if self.outlook_widget: # Mise à jour unique du TimerWidget if hasattr(self.outlook_widget, 'timer_widget'): self.outlook_widget.timer_widget.set_text(f"Display {display_time:.1f}s", "display") # *** OPTIMISATION 3 : Préparation en lot des onglets *** tabs_to_load = [] # Traitement détaillé de chaque email du lot for i, link_data in enumerate(batch): try: subject = link_data.get('subject', 'Sujet inconnu') # *** OPTIMISATION 4 : Mise à jour de bannière groupée *** if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser # Mise à jour de la bannière seulement pour le premier email du lot if i == 0 and hasattr(browser, 'bottom_banner') and getattr(self, 'banner_position', 'bottom') != "disabled": banner_text = f"📧 Lot de {len(batch)} emails - {subject[:30]}{'...' if len(subject) > 30 else ''}" if hasattr(browser.bottom_banner, 'show_banner'): browser.bottom_banner.show_banner(banner_text) self.outlook_widget.log_action(f"📱 Bannière mise à jour pour le lot") # *** OPTIMISATION 5 : Log groupé et moins verbeux *** if hasattr(self, 'outlook_widget') and i == 0: # Log détaillé seulement pour le premier self.outlook_widget.log_action( f"📧 " + "─" * 25 + f" LOT DE {len(batch)} EMAILS " + "─" * 25) # Afficher un échantillon des emails du lot for j, link_item in enumerate(batch[:3]): # Max 3 emails détaillés short_subject = link_item['subject'][:40] + "..." if len(link_item['subject']) > 40 else \ link_item['subject'] short_link = link_item['link'][:60] + "..." if len(link_item['link']) > 60 else \ link_item['link'] self.outlook_widget.log_action(f"📧 [{j + 1}] {short_subject}") self.outlook_widget.log_action(f"🔗 [{j + 1}] {short_link}") if len(batch) > 3: self.outlook_widget.log_action(f"📧 ... et {len(batch) - 3} autres emails") # *** OPTIMISATION 6 : Création d'onglets optimisée *** mini_tab = None if self.outlook_widget.mini_browser: if i == 0: mini_tab = self.outlook_widget.mini_browser.get_current_tab() if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"📱 Utilisation de l'onglet principal pour le lot") else: # *** OPTIMISATION 7 : Création d'onglets en lot *** if i == 1: # Premier onglet supplémentaire self.outlook_widget.log_action( f"➕ Création de {len(batch) - 1} onglets supplémentaires...") self.outlook_widget.mini_browser.add_tab() mini_tab = self.outlook_widget.mini_browser.get_current_tab() created_tabs.append(mini_tab) # Stocker les informations de l'onglet pour chargement groupé if mini_tab and mini_tab.web_view: tabs_to_load.append((mini_tab, link_data['link'], i)) # Navigateur principal (si visible) - même optimisation if hasattr(self.outlook_widget, 'browser_window') and self.outlook_widget.browser_window.isVisible(): if i == 0: main_tab = self.outlook_widget.browser_window.get_current_tab() else: self.outlook_widget.browser_window.add_tab() main_tab = self.outlook_widget.browser_window.get_current_tab() if main_tab and main_tab.web_view: main_tab.web_view.load(QUrl(link_data['link'])) if hasattr(self.outlook_widget.browser_window, 'console_header'): if hasattr(self.outlook_widget.browser_window.console_header, 'set_message'): self.outlook_widget.browser_window.console_header.set_message(subject) self.outlook_widget.browser_window.console_header.update_page_time( f"Display {display_time:.1f}s") if isinstance(self.outlook_widget.browser_window, BrowserWindow): self.outlook_widget.browser_window.update_url(QUrl(link_data['link'])) # *** OPTIMISATION 8 : Enregistrement en lot des liens traités *** if hasattr(self, 'duplicates_detection_enabled') and self.duplicates_detection_enabled: self.links_handler.add_processed_link( link_data['link'], metadata={ 'sender': link_data['sender'], 'subject': link_data['subject'], 'processed_date': datetime.now().isoformat() } ) clicked_links += 1 except Exception as e: error_count += 1 if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"❌ Erreur onglet {i + 1}: {str(e)}") # *** OPTIMISATION 9 : Chargement en lot des URLs *** if tabs_to_load: self.outlook_widget.log_action(f"🌐 Chargement simultané de {len(tabs_to_load)} URLs...") for mini_tab, url, index in tabs_to_load: mini_tab.web_view.load(QUrl(url)) if hasattr(self.outlook_widget.mini_browser, 'console_header') and index == 0: self.outlook_widget.mini_browser.console_header.update_page_time( f"Display {display_time:.1f}s") # Vérifier et enregistrer dans l'historique if isinstance(self.outlook_widget.mini_browser, BrowserWindow): self.outlook_widget.mini_browser.update_url(QUrl(url)) self.outlook_widget.log_action(f"✅ {len(tabs_to_load)} onglets chargés simultanément") # Mise à jour finale du compteur self.update_links_clicked() if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"📊 Compteur mis à jour: {clicked_links} liens traités") # *** OPTIMISATION 10 : Nettoyage COM optimisé *** try: import gc import pythoncom gc.collect() pythoncom.CoFreeUnusedLibraries() if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"🧹 Nettoyage COM après lot de {len(batch)} emails") except Exception as e: if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"⚠️ Erreur nettoyage COM: {str(e)}") # Phase d'affichage if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"👁️ " + "═" * 10 + " PHASE D'AFFICHAGE " + "═" * 10) self.outlook_widget.log_action( f"👁️ Affichage de {len(batch)} onglet(s) pendant {display_time:.1f} secondes...") # Attente unique pour tout le lot loop = QEventLoop() QTimer.singleShot(int(display_time * 1000), loop.quit) loop.exec_() if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"✅ Phase d'affichage terminée") # Phase de suppression des emails if self.auto_delete and hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"🗑️ " + "═" * 10 + " PHASE DE SUPPRESSION " + "═" * 10) # *** OPTIMISATION 11 : Suppression en lot *** def safe_delete_batch(): deleted_in_batch = 0 for i, link_data in enumerate(batch): try: link_data['message'].Delete() deleted_in_batch += 1 except: pass return deleted_in_batch try: self.outlook_widget.log_action(f"🗑️ Suppression en lot de {len(batch)} emails...") if self.safe_com_operation(safe_delete_batch, max_retries=2): deleted_count = len(batch) # Supposer que tout s'est bien passé self.outlook_widget.log_action(f"✅ {deleted_count} emails supprimés en lot") else: # Fallback : suppression individuelle for i, link_data in enumerate(batch): try: def safe_delete_single(): link_data['message'].Delete() return True if self.safe_com_operation(safe_delete_single, max_retries=1): deleted_count += 1 else: error_count += 1 except: error_count += 1 self.outlook_widget.log_action( f"📊 Suppression individuelle: {deleted_count}/{len(batch)} emails supprimés") except Exception as e: error_count += len(batch) if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"❌ Erreur suppression en lot: {str(e)}") # Phase de nettoyage des onglets if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"🧹 " + "═" * 10 + " NETTOYAGE DES ONGLETS " + "═" * 10) # *** OPTIMISATION 12 : Fermeture d'onglets optimisée *** if self.outlook_widget.mini_browser: tab_count = self.outlook_widget.mini_browser.tab_widget.count() if tab_count > 1: self.outlook_widget.log_action(f"🧹 Fermeture de {tab_count - 1} onglet(s) supplémentaire(s)...") # Fermer tous les onglets d'un coup au lieu d'un par un tabs_closed = 0 for i in range(tab_count - 1, 0, -1): try: self.outlook_widget.mini_browser.close_tab(i) tabs_closed += 1 except Exception as e: error_count += 1 if tabs_closed > 0: self.outlook_widget.log_action(f"✅ {tabs_closed} onglets fermés") if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"✅ Nettoyage terminé - 1 onglet conservé") # *** NOUVELLE SECTION : FERMETURE DES APPLICATIONS C++ *** if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"🔧 " + "═" * 10 + " FERMETURE APPLICATIONS C++ " + "═" * 10) self.outlook_widget.log_action(f"🔧 Fermeture des processus QtWebEngine après traitement du lot...") # Fermer tous les processus C++ (QtWebEngine) try: self.terminate_qt_processes() if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"✅ Processus C++ fermés pour le lot terminé") except Exception as cpp_error: error_count += 1 if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"❌ Erreur fermeture processus C++: {str(cpp_error)}") # Pause pour s'assurer que les processus sont bien fermés import time time.sleep(1) if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action( f"🔄 Processus C++ fermés - Ils se recréeront automatiquement pour le prochain lot") # Phase de pause if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"⏸️ " + "═" * 12 + " PHASE DE PAUSE " + "═" * 12) self.outlook_widget.log_action(f"⏸️ Pause de {between_time:.1f} secondes avant le prochain lot...") # Mettre à jour l'affichage du temps de pause if hasattr(self.outlook_widget, 'timer_widget'): self.outlook_widget.timer_widget.set_text(f"Pause {between_time:.1f}s", "pause") if hasattr(self.outlook_widget.mini_browser, 'console_header'): self.outlook_widget.mini_browser.console_header.update_page_time(f"Pause {between_time:.1f}s") if hasattr(self.outlook_widget.browser_window, 'console_header'): self.outlook_widget.browser_window.console_header.update_page_time(f"Pause {between_time:.1f}s") # Attente unique après le traitement du lot loop = QEventLoop() QTimer.singleShot(int(between_time * 1000), loop.quit) loop.exec_() # Effacer la bannière après le traitement if hasattr(self, 'outlook_widget') and hasattr(self.outlook_widget, 'mini_browser'): browser = self.outlook_widget.mini_browser if hasattr(browser, 'console_header') and hasattr(browser.console_header, 'set_message'): browser.console_header.set_message("Prêt") elif hasattr(browser, 'bottom_banner') and hasattr(browser.bottom_banner, 'hide_banner'): browser.bottom_banner.hide_banner() # Résumé final du lot - Version optimisée if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"🏁 " + "═" * 15 + " RÉSUMÉ DU LOT " + "═" * 15) self.outlook_widget.log_action( f"📊 Emails traités: {len(batch)} | Liens cliqués: {clicked_links} | Supprimés: {deleted_count}") if error_count > 0: self.outlook_widget.log_action(f"❌ Erreurs rencontrées: {error_count}") else: self.outlook_widget.log_action(f"✅ Aucune erreur") # Calcul du taux de succès success_rate = ((len(batch) - error_count) / len(batch)) * 100 if len(batch) > 0 else 0 self.outlook_widget.log_action(f"📈 Taux de succès: {success_rate:.1f}%") self.outlook_widget.log_action(f"🔧 Processus C++ : Fermés et prêts pour recréation") self.outlook_widget.log_action(f"🏁 " + "═" * 55) return clicked_links except Exception as e: error_count += 1 if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"❌ " + "═" * 10 + " ERREUR CRITIQUE " + "═" * 10) self.outlook_widget.log_action(f"❌ Erreur lors du traitement du lot : {str(e)}") import traceback error_details = traceback.format_exc() if hasattr(self, 'outlook_widget'): # Afficher seulement les lignes importantes du traceback error_lines = error_details.split('\n')[-3:-1] for line in error_lines: if line.strip(): self.outlook_widget.log_action(f"🔍 {line.strip()}") # En cas d'erreur, tenter de fermer tous les onglets créés if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"🛠️ Tentative de nettoyage d'urgence...") if self.outlook_widget and self.outlook_widget.mini_browser: tab_count = self.outlook_widget.mini_browser.tab_widget.count() emergency_closed = 0 for i in range(tab_count - 1, 0, -1): try: self.outlook_widget.mini_browser.close_tab(i) emergency_closed += 1 except: pass if hasattr(self, 'outlook_widget') and emergency_closed > 0: self.outlook_widget.log_action(f"🛠️ {emergency_closed} onglets fermés en urgence") # *** NETTOYAGE D'URGENCE DES PROCESSUS C++ EN CAS D'ERREUR *** if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"🛠️ Nettoyage d'urgence des processus C++...") try: self.terminate_qt_processes() if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"✅ Processus C++ fermés en urgence") except Exception as cleanup_error: if hasattr(self, 'outlook_widget'): self.outlook_widget.log_action(f"❌ Erreur nettoyage d'urgence C++: {str(cleanup_error)}") return clicked_links def record_action_in_main_window(self, source, link): """Envoie les données de l'action à la fenêtre principale""" if hasattr(self, 'main_window') and hasattr(self.main_window, 'record_action'): self.main_window.record_action(source, link, self.reward_value) def _find_valid_link_optimized(self, message, link_keywords, mode="url"): """ Version optimisée de find_valid_link qui évite les logs inutiles et améliore les performances avec un cache des mots-clés CORRECTION: Utilise le paramètre mode au lieu de self.link_mode """ try: # Si le mode pattern est spécifié, utiliser la méthode pattern if mode == "pattern": self.log_action("🎯 Mode PATTERN activé - Recherche contextuelle") return self.find_valid_link_pattern(message, link_keywords) # Mode URL classique OPTIMISÉ self.log_action("🔗 Mode URL classique activé") # *** OPTIMISATION 1 : Récupération sécurisée du body *** try: body = self._safe_get_body(message) if hasattr(self, '_safe_get_body') else getattr(message, 'Body', '') if not body: return None except Exception as e: self.log_action(f"❌ Erreur lors de la récupération du corps : {e}") return None # *** OPTIMISATION 2 : Pattern regex plus efficace *** import re # Utiliser le pattern original mais optimisé url_pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+' links = re.findall(url_pattern, body, re.IGNORECASE) if not links: return None self.log_action(f"📝 Body récupéré (brut), longueur : {len(body)}") self.log_action(f"🔍 {len(links)} liens trouvés dans le message") # *** OPTIMISATION 3 : Mots-clés d'exclusion optimisés *** exclude_keywords = ['delete', 'unsubscribe', 'remove', 'vacation', 'report', 'login', 'helpdesk', 'support', 'assistance', 'opt-out', 'preferences'] # *** OPTIMISATION 4 : Cache des mots-clés pour éviter les calculs répétitifs *** keyword_cache = set(kw.lower() for kw in link_keywords) exclude_cache = set(ex_kw.lower() for ex_kw in exclude_keywords) # Vérifier si le dernier lien contient un mot-clé d'exclusion if links: last_link_lower = links[-1].lower() if any(ex_kw in last_link_lower for ex_kw in exclude_cache): links = links[:-1] # Exclure le dernier lien self.log_action("⛔ Dernier lien exclu (mot-clé d'exclusion)") if not links: return None # *** OPTIMISATION 5 : Parcours optimisé des liens *** for i, link in enumerate(links): # *** OPTIMISATION 6 : Nettoyage optimisé du lien *** clean_link = link.strip() clean_link = re.sub(r'[<>"\'\s]+$', '', clean_link) # Optimisé avec + self.log_action(f"🔗 Analyse du lien {i + 1}/{len(links)}: {clean_link}") # *** OPTIMISATION 7 : Vérification rapide avec cache *** clean_link_lower = clean_link.lower() # Vérification rapide des exclusions excluded_keywords_found = [ex_kw for ex_kw in exclude_keywords if ex_kw.lower() in clean_link_lower] if excluded_keywords_found: self.log_action(f"⛔ Lien ignoré (mots-clés d'exclusion) : {', '.join(excluded_keywords_found)}") continue # Vérification rapide des mots-clés valides avec le cache found_keywords = [kw for kw in keyword_cache if kw in clean_link_lower] if found_keywords: # Reconstituer les mots-clés originaux pour l'affichage original_keywords = [] for orig_keyword in link_keywords: # Utiliser le paramètre au lieu de self.link_keywords if orig_keyword.lower() in found_keywords: # Gestion spéciale pour les paramètres d'URL if '=' in clean_link_lower and orig_keyword.lower() + '=' in clean_link_lower: original_keywords.append(f"{orig_keyword}={orig_keyword.lower()}") else: original_keywords.append(orig_keyword) self.log_action(f"✅ Mots-clés valides trouvés dans le lien : {', '.join(original_keywords)}") self.log_action(f"✅ Lien valide sélectionné (mode URL) : {clean_link}") return clean_link else: # *** OPTIMISATION 8 : Log conditionnel - ne pas spammer avec "aucun mot-clé valide" *** # Seulement log si c'est le dernier lien et qu'aucun n'a été trouvé if i == len(links) - 1: # Dernier lien self.log_action("❌ Aucun lien valide trouvé en mode URL") return None except Exception as e: self.log_action(f"❌ Erreur lors de l'analyse des liens: {str(e)}") return None def continue_processing(self, link): if self.outlook_widget: self.outlook_widget.log_action(f"Attente de {self.pause_time} secondes pour l'affichage") QTimer.singleShot(self.pause_time * 1000, lambda: self.finish_processing(link)) def finish_processing(self, link): if self.outlook_widget: self.outlook_widget.log_action(f"Traitement terminé pour le lien : {link}") # Vérifier si c'est le dernier lien if self.is_last_link_processed(): # Actions de fin de traitement directement ici # Vérifier si l'option de verrouillage est activée if (hasattr(self.outlook_widget, 'lock_session_checkbox') and self.outlook_widget.lock_session_checkbox.isChecked()): self.outlook_widget.log_action("Verrouillage de la session Windows...") # Méthode 1: Utiliser ctypes try: import ctypes ctypes.windll.user32.LockWorkStation() except Exception as e: self.outlook_widget.log_action(f"Erreur méthode 1: {str(e)}") # Méthode 2: Utiliser os.system try: import os os.system('rundll32.exe user32.dll,LockWorkStation') except Exception as e2: self.outlook_widget.log_action(f"Erreur méthode 2: {str(e2)}") # Vérifier si l'option d'extinction est activée if (hasattr(self.outlook_widget, 'shutdown_pc_checkbox') and self.outlook_widget.shutdown_pc_checkbox.isChecked()): self.outlook_widget.log_action("Programmation de l'extinction du PC...") try: import os os.system('shutdown /s /t 30 /c "Extinction automatique programmée par l\'application."') except Exception as e: self.outlook_widget.log_action(f"Erreur extinction: {str(e)}") def find_first_valid_link(self, message, link_keywords): links = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', message.Body) for link in links: if any(keyword.lower() in link.lower() for keyword in link_keywords) and 'delete.php' not in link.lower(): return link return None def close_browser(self): if self.browser: print("Tentative de fermeture du navigateur...") self.browser.close() print("Navigateur fermé") else: print("Pas de navigateur à fermer") def should_process_message(self, message, sender_keyword, subject_keywords): """ Vérifie si un message doit être traité en fonction des critères donnés """ if message is None: return False try: # Vérification de l'expéditeur if sender_keyword: sender_email = getattr(message, 'SenderEmailAddress', '').lower() if sender_keyword.lower() not in sender_email: return False # Vérification des mots-clés du sujet if subject_keywords and hasattr(message, 'Subject') and not any( keyword.lower() in message.Subject.lower() for keyword in subject_keywords): return False return True except Exception as e: add_debug_log("OutlookHandler.should_process_message", f"Erreur lors de la vérification du message : {e}") print(f"Erreur lors de la vérification du message : {e}") return False def set_pause_time(self, value): self.pause_time = value print(f"Temps de pause défini à {self.pause_time} secondes") def process_message(self, message, link_keywords): self.log_event.emit(f"Traitement du message de {message.SenderEmailAddress}") try: links = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', message.Body) clicked_links = 0 print(f"Liens trouvés dans le message : {links}") valid_links = [link for link in links if any( keyword.lower() in link.lower() for keyword in link_keywords) and 'delete.php' not in link.lower()] for link in valid_links: print(f"Tentative de clic sur le lien : {link}") if self.browser: try: self.browser.load_with_random_delay(link) QApplication.instance().postEvent(self.outlook_widget, QCustomEvent(QEvent.User, link)) clicked_links += 1 self.increment_links_clicked() print(f"Lien cliqué avec succès : {link}") time.sleep(self.pause_time) except Exception as e: print(f"Erreur lors de l'ouverture du lien {link}: {str(e)}") import traceback traceback.print_exc() finally: try: self.browser.close() except Exception as e: print(f"Erreur lors de la fermeture du navigateur: {str(e)}") else: print(f"Lien trouvé mais non ouvert (pas de navigateur disponible) : {link}") return clicked_links except Exception as e: print(f"Erreur lors du traitement du message : {str(e)}") import traceback traceback.print_exc() return 0 def _get_subfolders(self, folder, prefix=''): """Récupère les sous-dossiers de manière récursive avec indentation""" folders = [] try: for subfolder in folder.Folders: # Stocker l'information sur la présence de sous-dossiers has_subfolders = subfolder.Folders.Count > 0 folders.append({ 'name': subfolder.Name, 'id': subfolder.EntryID, 'level': len(prefix) // 2, 'has_subfolders': has_subfolders, 'is_expanded': False, # Nouvel attribut pour suivre l'état 'display_name': f"{prefix}{'▶ ' if has_subfolders else ' '}{subfolder.Name}" # ▶ par défaut }) # Récursivement ajouter les sous-dossiers avec indentation supplémentaire folders.extend(self._get_subfolders(subfolder, prefix + ' ')) except Exception as e: print(f"Erreur lors de la récupération des sous-dossiers : {str(e)}") return folders def get_folders(self, mailbox): """Récupère uniquement la boîte de réception et ses sous-dossiers""" try: if not self.outlook and not self.connect(): raise Exception("Impossible de se connecter à Outlook") # Récupérer le dossier racine de la boîte mail root_folder = next((f for f in self.outlook.Folders if f.Name == mailbox), None) if not root_folder: raise Exception(f"Boîte mail '{mailbox}' non trouvée") # Obtenir la boîte de réception inbox = root_folder.Folders["Boîte de réception"] # Commencer avec la boîte de réception folders = [{ 'name': inbox.Name, 'id': inbox.EntryID, 'level': 0, 'has_subfolders': inbox.Folders.Count > 0, 'is_expanded': False, 'display_name': f"▶ {inbox.Name}" if inbox.Folders.Count > 0 else inbox.Name }] # Ajouter récursivement ses sous-dossiers folders.extend(self._get_subfolders(inbox)) return folders except Exception as e: print(f"Erreur lors de la récupération des dossiers : {e}") return [] def set_reward_value(self, value): self.reward_value = float(value) if value is not None else None def reset_reward_value(self): self.reward_value = None self.links_clicked = 0 self.total_reward = 0.0 print("Valeur de récompense et compteurs réinitialisés") def increment_links_clicked(self): self.links_clicked += 1 if self.reward_value is not None: self.total_reward = self.reward_value * self.links_clicked print(f"Liens cliqués incrémenté. Nouveau total : {self.links_clicked}") def get_reward_info(self): if self.reward_value is None: return "Récompense non définie | Liens cliqués: 0" return f"Récompense totale: {self.total_reward:.2f} | Liens cliqués: {self.links_clicked}" def cleanup_expired_emails(self, mailbox=None, folder=None, sender=None, subject_keywords=None, link_keywords=None, max_emails=None, validity_days=None, progress_callback=None): """ NOUVELLE MÉTHODE - Nettoie les emails périmés avec retour détaillé """ try: self.log_action("🧹 Début du nettoyage des emails") # Connexion à Outlook if not self.outlook and not self.connect(): raise Exception("Impossible de se connecter à Outlook") # Récupération du dossier root_folder = next((f for f in self.outlook.Folders if f.Name == mailbox), None) if not root_folder: raise Exception(f"Boîte mail '{mailbox}' non trouvée") target_folder = self.find_folder(root_folder, folder) if not target_folder: raise Exception(f"Dossier '{folder}' non trouvé") # Initialisation des compteurs valid_count = 0 expired_count = 0 deleted_count = 0 current_time = datetime.now() # Récupération et tri des messages messages = target_folder.Items messages.Sort("[ReceivedTime]", True) total_messages = messages.Count processed_count = 0 self.log_action(f"📊 {total_messages} emails à analyser dans le dossier '{folder}'") # Listes pour stocker les détails expired_details = [] deletion_errors = [] # Traitement des messages messages_list = list(messages) # Convertir en liste pour éviter les problèmes de modification for message in messages_list: if self.should_stop: break processed_count += 1 if progress_callback: progress_callback(processed_count, total_messages) if max_emails and (valid_count + expired_count) >= max_emails: break # Vérification des critères de l'email if self.should_process_message(message, sender, subject_keywords): if validity_days is not None: try: received_time = message.ReceivedTime.replace(tzinfo=None) age_days = (current_time - received_time).days if age_days <= validity_days: # Email encore valide if self.find_valid_link(message, link_keywords): valid_count += 1 self.log_action(f"✅ Email valide conservé: {message.Subject[:50]}...") else: # Email périmé - à supprimer expired_count += 1 # Détails de l'email périmé email_info = { 'subject': message.Subject, 'sender': getattr(message, 'SenderEmailAddress', 'Inconnu'), 'received_date': received_time.strftime('%d/%m/%Y %H:%M'), 'age_days': age_days } expired_details.append(email_info) # SUPPRESSION EFFECTIVE try: subject = message.Subject if self.permanent_delete_enabled and self.apply_to_count: # Suppression définitive message.Delete(0) self.log_action(f"🗑️ SUPPRIMÉ DÉFINITIVEMENT: {subject[:50]}...") else: # Suppression normale (vers la corbeille) message.Delete() self.log_action(f"🗑️ Déplacé vers corbeille: {subject[:50]}...") deleted_count += 1 except Exception as delete_error: error_info = { 'subject': message.Subject, 'error': str(delete_error) } deletion_errors.append(error_info) self.log_action( f"❌ Erreur suppression: {message.Subject[:50]}... - {str(delete_error)}") except Exception as e: self.log_action(f"❌ Erreur traitement message: {str(e)}") continue else: # Pas de critère de validité - juste compter if self.find_valid_link(message, link_keywords): valid_count += 1 # Affichage du résumé détaillé self.log_action("📊 " + "=" * 50) self.log_action("🏁 NETTOYAGE TERMINÉ") self.log_action("📊 " + "=" * 50) self.log_action(f"📧 Emails analysés: {processed_count}") self.log_action(f"✅ Emails valides conservés: {valid_count}") self.log_action(f"⏰ Emails périmés détectés: {expired_count}") self.log_action(f"🗑️ Emails effectivement supprimés: {deleted_count}") if deletion_errors: self.log_action(f"❌ Erreurs de suppression: {len(deletion_errors)}") return { 'valid_count': valid_count, 'expired_count': expired_count, 'deleted_count': deleted_count, 'processed_count': processed_count, 'expired_details': expired_details, 'deletion_errors': deletion_errors } except Exception as e: self.log_action(f"❌ Erreur lors du nettoyage : {str(e)}") raise class OutlookWidget(QWidget): start_scan_signal = pyqtSignal() stop_scan_signal = pyqtSignal() def __init__(self, parent, outlook_handler=None, imap_handler=None): super().__init__(parent) # === INITIALISATION DE BASE === self.font_size = 12 self.outlook_handler = outlook_handler self.imap_handler = imap_handler self.main_window = parent self.current_profile = None # Raccourci Ctrl+L pour remplissage automatique from PyQt5.QtWidgets import QShortcut from PyQt5.QtGui import QKeySequence fill_shortcut = QShortcut(QKeySequence("Ctrl+L"), self) fill_shortcut.activated.connect(self.auto_fill_credentials) # === DÉTECTION DU MODE DE CONNEXION === self.detected_connection_mode = self.detect_connection_mode() # === INITIALISATION DES ÉTATS === self.is_scanning = False self.is_counting = False self.is_mini_browser = True self.current_scan_active = False self.link_mode = "url" # Mode de filtrage actif self.fields_empty = True # État initial: champs vides self.time_display_paused = False # === INITIALISATION DES DONNÉES === self.cycle_stats = [] self.complete_log_history = [] # Tous les messages de log self.scan_log_history = [] # Historique spécifique au scan en cours self.report_data = [] # Données pour le rapport self.deleted_emails = [] # Emails supprimés self.current_urls = [] self.carousel_index = 0 self.emails_to_process = [] self.last_profile_data = None self.doublons_count = 0 self.remaining_time = 0 # === CONFIGURATION DU LAYOUT PRINCIPAL === self.main_interface_layout = QVBoxLayout(self) # === INITIALISATION DES TIMERS === self.log_clear_timer = QTimer() self.log_clear_timer.timeout.connect(self.update_log_timer) self.log_clear_remaining = 120 self.scan_timer = QTimer(self) self.scan_timer.timeout.connect(self.update_time_display) self.task_update_timer = QTimer(self) self.task_update_timer.timeout.connect(self.update_next_task_display) # === INITIALISATION DES PARAMÈTRES === self.settings = QSettings("VotreEntreprise", "AutoLook") self.set_styles() # === CRÉATION DE L'INTERFACE === self.create_widgets() # === INITIALISATION POST-CRÉATION === # Créer le log_text s'il n'existe pas déjà (normalement créé dans create_widgets) if not hasattr(self, 'log_text'): self.log_text = QTextEdit() self.log_text.setReadOnly(True) # === CONFIGURATION DU CYCLE PAUSE TIMER === # Créer le cycle_pause_timer s'il n'existe pas if not hasattr(self, 'cycle_pause_timer'): from your_module import CyclePauseTimer # Ajustez l'import selon votre structure self.cycle_pause_timer = CyclePauseTimer() # Trouver et ajouter le cycle_pause_timer au bon endroit self._setup_cycle_pause_timer() # === CONNEXIONS DES HANDLERS === if self.outlook_handler: self.outlook_handler.log_event.connect(self.display_log) self.outlook_handler.log_action("✅ Système de logging initialisé") self.outlook_handler.scan_progress.connect(self.update_progress) self.outlook_handler.links_clicked_updated.connect(self.update_links_clicked_display) elif self.imap_handler: self.display_log("✅ Mode IMAP actif - Connexion initialisée") # === RESTAURATION DES PARAMÈTRES === self.restore_tips_mode() # === DÉMARRAGE DES TIMERS === if hasattr(self, 'update_next_task_display'): self.update_next_task_display() self.task_update_timer.start(5000) # Toutes les 5 secondes # === LOG DE CONFIRMATION === self.log_action("✨ OutlookWidget initialisé avec succès") def _setup_cycle_pause_timer(self): """Configure le cycle pause timer après création des widgets""" try: # Chercher le groupe de progression for child in self.findChildren(QGroupBox): if child.title() == "Progression et Statut": progress_group = child if progress_group.layout(): progress_group.layout().addWidget(self.cycle_pause_timer) self.cycle_pause_timer.hide() # Masqué initialement self.log_action("✅ Cycle pause timer configuré") break else: self.log_action("⚠️ Groupe 'Progression et Statut' non trouvé") except Exception as e: self.log_action(f"❌ Erreur configuration cycle pause timer: {e}") # Ajouter dans OutlookWidget si nécessaire : def update_imap_accounts(self, results): """Met à jour l'interface avec les résultats IMAP""" pass def detect_connection_mode(self): """Détecte automatiquement le mode de connexion actuel""" try: if self.outlook_handler is not None: self.log_action("🟢 Mode détecté : Outlook (COM)") return "outlook" elif self.imap_handler is not None: self.log_action("🔵 Mode détecté : IMAP") return "imap" else: # Vérifier si Thunderbird est disponible (ajustez selon votre logique) try: # Ici vous pouvez ajouter la logique de détection de Thunderbird # Par exemple, vérifier un fichier de configuration ou un processus import psutil for proc in psutil.process_iter(['name']): if 'thunderbird' in proc.info['name'].lower(): self.log_action("🟠 Mode détecté : Thunderbird") return "thunderbird" except: pass self.log_action("⚪ Aucun mode de connexion détecté") return None except Exception as e: self.log_action(f"❌ Erreur détection mode connexion : {e}") return None def update_cycle_status(self, cycle_number, emails_in_cycle, remaining_emails, pause_time=None): """ Met à jour l'affichage du statut du cycle actuel """ try: if pause_time is not None: # En pause entre les cycles - formater le temps de pause pause_str = f"{pause_time:.1f}" if isinstance(pause_time, float) else str(pause_time) status_text = ( f"Cycle {cycle_number} terminé. Pause de {pause_str}s avant le cycle {cycle_number + 1}. " f"{remaining_emails} emails restants.") # Activer l'afficheur de pause entre cycles if hasattr(self, 'cycle_pause_timer'): self.cycle_pause_timer.start_countdown(float(pause_time), cycle_number + 1) else: # En cours de traitement d'un cycle status_text = (f"Traitement du cycle {cycle_number}: {emails_in_cycle} emails. " f"{remaining_emails} emails restants.") # S'assurer que l'afficheur de pause est masqué if hasattr(self, 'cycle_pause_timer'): self.cycle_pause_timer.hide() # Mettre à jour le libellé de statut if hasattr(self, 'status_label'): self.status_label.setText(status_text) # Journaliser le statut self.log_action(status_text) except Exception as e: logger.error(f"Erreur dans update_cycle_status: {str(e)}") def display_log(self, message): """Affiche le message dans le QTextEdit avec formatage""" try: timestamp = QDateTime.currentDateTime().toString("HH:mm:ss") log_entry = f"{timestamp} - {message}" # Si le message est déjà au format HTML (contient des balises span) if "{log_entry}" self.log_text.append(formatted_entry) # Scroll automatique scrollbar = self.log_text.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) # Force la mise à jour immédiate QApplication.processEvents() except Exception as e: print(f"Erreur lors de l'affichage du log: {e}") def toggle_logging(self): """Gère l'activation/désactivation du journal avec transition""" try: from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QTimer is_logging_disabled = self.toggle_log_button.isChecked() # Trouver le layout parent qui contient log_text log_container = self.log_text.parent() log_layout = log_container.layout() if is_logging_disabled: # Masquer le journal self.log_text.hide() # Créer et afficher la bannière si elle n'existe pas if not hasattr(self, 'banner_container'): # Créer une bannière simple à la place from PyQt5.QtWidgets import QLabel self.banner_container = QLabel( "Journal désactivé - Cliquez sur 'Activer le journal' pour réactiver") self.banner_container.setStyleSheet(""" QLabel { background-color: #2c3e50; color: #42effb; padding: 20px; border-radius: 4px; font-size: 14px; font-weight: bold; text-align: center; } """) log_layout.addWidget(self.banner_container) else: self.banner_container.show() # Changer le texte du bouton self.toggle_log_button.setText("Activer le journal") else: # Réafficher le journal if hasattr(self, 'banner_container'): self.banner_container.hide() if hasattr(self.banner_container, 'deleteLater'): self.banner_container.deleteLater() delattr(self, 'banner_container') self.log_text.show() self.log_text.clear() self.toggle_log_button.setText("Désactiver le journal") self.log_action("✅ Journal réactivé") # Forcer la mise à jour de l'interface QApplication.processEvents() except Exception as e: print(f"Erreur toggle logging: {e}") def _show_banner(self): """Affiche la bannière avec un effet de fondu""" try: self.log_text.hide() if not hasattr(self, 'banner_container'): self.banner_container = FlipBanner() # Ajouter la bannière au même parent et au même index que log_text parent_layout = self.log_text.parent().layout() log_text_index = parent_layout.indexOf(self.log_text) parent_layout.insertWidget(log_text_index, self.banner_container) self.banner_container.show() self.banner_container.setWindowOpacity(0.0) fade_in = QPropertyAnimation(self.banner_container, b"windowOpacity") fade_in.setDuration(300) fade_in.setStartValue(0.0) fade_in.setEndValue(1.0) fade_in.start() except Exception as e: print(f"Erreur dans _show_banner: {e}") self.log_action(f"Erreur lors de l'affichage de la bannière: {str(e)}") def _show_log(self): """Réaffiche le journal avec un effet de fondu""" try: if hasattr(self, 'banner_container'): self.banner_container.gif.stop() self.banner_container.animation.stop() self.banner_container.hide() self.banner_container.deleteLater() delattr(self, 'banner_container') self.toggle_log_button.setText("Désactiver le journal") self.log_text.show() self.log_text.setWindowOpacity(0.0) fade_in = QPropertyAnimation(self.log_text, b"windowOpacity") fade_in.setDuration(300) fade_in.setStartValue(0.0) fade_in.setEndValue(1.0) fade_in.start() self.log_text.clear() self.log_action("✅ Journal réactivé") except Exception as e: print(f"Erreur dans _show_log: {e}") def get_current_state(self): """Récupère l'état actuel des paramètres pour la sauvegarde""" state = { "search_criteria": { "sender": self.sender_entry.text(), "subject_keywords": self.subject_entry.text(), "link_keywords": self.link_entry.text(), "max_emails": self.max_emails_entry.text() }, "settings": { "pause_time": self.outlook_handler.pause_time, "links_count": self.outlook_handler.links_count, "reward_value": self.outlook_handler.reward_value, "validity": { "enabled": self.validity_checkbox.isChecked(), "days": int( self.validity_entry.text()) if self.validity_checkbox.isChecked() and self.validity_entry.text() else None } } } if self.mailbox_listbox.currentItem(): state["mailbox"] = self.mailbox_listbox.currentItem().text() if self.folder_listbox.currentItem(): state["folder"] = self.folder_listbox.currentItem().text() return state def reset_search_criteria(self): """Réinitialise tous les champs de critères de recherche""" self.sender_entry.clear() self.subject_entry.clear() self.link_entry.clear() self.max_emails_entry.clear() self.reward_entry.clear() # Réinitialiser aussi les cases à cocher if hasattr(self, 'validity_checkbox'): self.validity_checkbox.setChecked(False) if hasattr(self, 'validity_entry'): self.validity_entry.clear() self.validity_entry.setVisible(False) if hasattr(self, 'auth_checkbox'): self.auth_checkbox.setChecked(False) # Indication visuelle self.status_label.setText("Critères de recherche réinitialisés") self.log_action("✨ Tous les critères de recherche ont été réinitialisés") def apply_profile(self, profile_data): """Applique un profil chargé""" try: if "search_criteria" in profile_data: self.apply_search_criteria(profile_data["search_criteria"]) if "settings" in profile_data: self.apply_settings(profile_data["settings"]) # Appliquer les paramètres de validité if "validity" in profile_data["settings"]: validity_settings = profile_data["settings"]["validity"] self.validity_checkbox.setChecked(validity_settings.get("enabled", False)) if validity_settings.get("enabled"): self.validity_entry.setText(str(validity_settings.get("days", ""))) self.validity_entry.setVisible(True) else: self.validity_entry.clear() self.validity_entry.setVisible(False) if "mailbox" in profile_data: self.select_mailbox(profile_data["mailbox"]) if "folder" in profile_data: self.select_folder(profile_data["folder"]) self.fields_empty = False self.reset_reload_button.setText("Réinitialiser") self.last_profile_data = profile_data # Sauvegarder ce profil comme dernier utilisé self.log_action("Profil appliqué avec succès") return True except Exception as e: self.log_action(f"Erreur lors de l'application du profil: {str(e)}") return False def load_profile(self, item): try: profile_name = self.profile_list.currentText() if not profile_name: return except Exception as e: QMessageBox.critical(self, "Erreur", f"Impossible de charger le profil : {str(e)}") def update_links_clicked(self, increment=1): self.links_clicked += increment if self.reward_value is not None: self.total_reward = self.reward_value * self.links_clicked self.links_clicked_updated.emit(self.links_clicked) def update_links_clicked_display(self, count): self.status_label.setText(f"Liens traités : {count}") self.update_reward_display() QApplication.processEvents() def on_start_button_clicked(self): """ Gère le clic sur 'Démarrer le scan' en fonction du toggle (direct → start_scan, indirect → start_scan_cached). """ # On récupère le flag depuis MainWindow (populé dans setup_interface) if getattr(self, 'main_window', None) and self.main_window.use_cached_processing: # Mode indirect : appelle start_scan_cached() self.start_scan_cached() else: # Mode direct : appelle start_scan() existant self.start_scan() def start_scan(self): """Démarre le scan avec garantie de sauvegarde complète des logs""" # === VÉRIFICATIONS PRÉLIMINAIRES === if self.link_mode != "url": self.log_action("🔁 Redirection vers le scan pattern (mode activé)") self.start_scan_pattern() return if not self.validate_inputs_for_scan(): self.log_action("Validation des entrées pour le scan échouée") return link_keywords = self.link_entry.text().split() if hasattr(self, 'link_entry') and self.link_entry.text() else [] # === INITIALISATION DU SCAN ET CAPTURE DES LOGS === print("=== DÉBUT INITIALISATION SCAN ===") # Marquer le début du scan et initialiser l'historique self.current_scan_active = True self.scan_log_history = [] print(f"INIT: scan_log_history initialisé, taille: {len(self.scan_log_history)}") # Définir le type de journal self.set_log_type("Traitement Mail") # Créer immédiatement le nom du journal avec horodatage from datetime import datetime current_time = datetime.now().strftime("%d/%m/%Y %H:%M") self.current_log_name = f"Traitement Mail - {current_time}" print(f"INIT: Journal nommé: {self.current_log_name}") # === RÉINITIALISATION DES DONNÉES === self.report_data = [] self.deleted_emails = [] self.outlook_handler.links_clicked = 0 self.outlook_handler.links_skipped = 0 # Effacer le contenu du QTextEdit self.log_text.clear() self.log_action("🚀 Nouveau traitement démarré", 'email') # Nettoyage initial avant le scan self.cleanup_browser_resources() QTimer.singleShot(500, lambda: None) # Pause pour laisser le nettoyage se faire # === CONFIGURATION DU SYSTÈME DE LOG === self.log_categories = { 'email': {'color': '#98FB98', 'icon': '📧', 'enabled': True}, 'count': {'color': '#FFB347', 'icon': '🔢', 'enabled': True}, 'error': {'color': '#FF6B6B', 'icon': '❌', 'enabled': True}, 'success': {'color': '#4CAF50', 'icon': '✅', 'enabled': True}, 'system': {'color': '#87CEEB', 'icon': '⚙️', 'enabled': True} } # === INITIALISATION DE L'INTERFACE === self.is_scanning = True self.set_scanning_state(True) self.status_label.setText("Scan en cours...") self.progress_bar.setValue(0) try: # === RÉCUPÉRATION DES PARAMÈTRES === mailbox = self.mailbox_listbox.currentItem().text() folder = self.folder_listbox.currentItem().text() sender = self.sender_entry.text() subject_keywords = self.subject_entry.text().split() # Vérification du mode URL sans mots-clés if self.link_mode == "url" and not link_keywords: self.log_action("⚠️ Aucun mot-clé de lien saisi — bascule en mode Keyword Automatique") # Bascule vers mode pattern self.link_mode = "pattern" # Bascule visuelle du bouton toggle if hasattr(self, 'toggle_click_mode_button'): self.toggle_click_mode_button.setChecked(True) self.toggle_click_mode_button.setText("Mode : Pattern") # Masquer le champ de lien if hasattr(self, 'link_entry'): self.link_entry.setVisible(False) # Sauvegarder le journal actuel avant de basculer self.current_scan_active = False self.auto_save_complete_scan_log() # Lancer la méthode pattern et quitter self.start_scan_pattern() return max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None # === CALCUL DE LA DURÉE ESTIMÉE === avg_pause_time = (self.outlook_handler.min_pause_time + self.outlook_handler.max_pause_time) / 2 avg_between_time = ( self.outlook_handler.min_between_pages_time + self.outlook_handler.max_between_pages_time) / 2 estimated_duration = max_emails * (avg_pause_time + avg_between_time) if max_emails else 0 # Initialiser et démarrer le timer self.remaining_time = int(estimated_duration) self.scan_timer.start(1000) # === CONNEXION DES SIGNAUX === self.outlook_handler.scan_progress.connect(self.update_progress) if hasattr(self.outlook_handler, 'cycle_status_update'): self.outlook_handler.cycle_status_update.connect(self.update_cycle_status) # === RÉINITIALISATION DES COMPTEURS === self.outlook_handler.links_clicked = 0 self.outlook_handler.links_skipped = 0 # === AFFICHAGE DES PARAMÈTRES DE CYCLE === if hasattr(self.outlook_handler, 'cycle_processing_enabled') and self.outlook_handler.cycle_processing_enabled: self.log_action( f"🔄 Configuration cycles: {self.outlook_handler.min_emails_per_cycle}-{self.outlook_handler.max_emails_per_cycle} emails par cycle") self.log_action( f"⏱️ Temps de pause: {self.outlook_handler.min_pause_between_cycles}-{self.outlook_handler.max_pause_between_cycles} secondes") # === PARTAGE CRITIQUE DE L'HISTORIQUE AVEC OUTLOOK_HANDLER === print(f"PARTAGE: Avant partage, scan_log_history taille: {len(self.scan_log_history)}") self.outlook_handler.scan_log_history = self.scan_log_history self.outlook_handler.current_scan_active = True print("PARTAGE: Historique partagé avec outlook_handler") print( f"PARTAGE: outlook_handler.current_scan_active = {getattr(self.outlook_handler, 'current_scan_active', 'NON_DÉFINI')}") # === LANCEMENT DU SCAN === self.log_action(f"🎯 Début du scan : {max_emails} emails maximum", 'system') print("SCAN: Appel de outlook_handler.scan_emails") clicked_links, matching_emails = self.outlook_handler.scan_emails( mailbox, folder, sender, subject_keywords, link_keywords, max_emails ) print(f"SCAN: Terminé - clicked_links={clicked_links}, matching_emails={matching_emails}") # === TRAITEMENT IMMÉDIAT DE FIN DE SCAN === if self.is_scanning: self.log_action("🏁 Scan terminé avec succès", 'success') self.status_label.setText("Scan terminé.") self.count_label.setText(f"Nombre d'emails correspondants: {matching_emails}") self.update_reward_display() # *** POINT CRITIQUE : ARRÊTER LA CAPTURE ET SAUVEGARDER IMMÉDIATEMENT *** print("SAUVEGARDE: Arrêt de la capture des logs") self.current_scan_active = False # Récupérer les logs depuis outlook_handler si nécessaire if hasattr(self.outlook_handler, 'scan_log_history') and len( self.outlook_handler.scan_log_history) > len(self.scan_log_history): print( f"RÉCUPÉRATION: Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler") self.scan_log_history = self.outlook_handler.scan_log_history.copy() print(f"SAUVEGARDE: scan_log_history final contient {len(self.scan_log_history)} entrées") # Sauvegarder immédiatement AVANT tout reset try: print("SAUVEGARDE: Début sauvegarde immédiate") result = self.auto_save_complete_scan_log() print(f"SAUVEGARDE: Résultat = {result}") if result: print("SAUVEGARDE: ✅ SUCCÈS - Journal sauvegardé avec horodatage") else: print("SAUVEGARDE: ❌ ÉCHEC - Aucun fichier créé") except Exception as save_error: print(f"SAUVEGARDE: ❌ ERREUR: {save_error}") import traceback traceback.print_exc() else: self.log_action("⛔ Scan arrêté par l'utilisateur", 'system') self.status_label.setText("Scan arrêté.") # Même traitement pour scan arrêté self.current_scan_active = False # Récupérer les logs depuis outlook_handler if hasattr(self.outlook_handler, 'scan_log_history') and len( self.outlook_handler.scan_log_history) > len(self.scan_log_history): print( f"RÉCUPÉRATION (arrêt): Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler") self.scan_log_history = self.outlook_handler.scan_log_history.copy() # Sauvegarder même en cas d'arrêt try: print("SAUVEGARDE (arrêt): Début sauvegarde") result = self.auto_save_complete_scan_log() print(f"SAUVEGARDE (arrêt): Résultat = {result}") except Exception as save_error: print(f"SAUVEGARDE (arrêt): ❌ ERREUR: {save_error}") except Exception as e: # === GESTION DES ERREURS === self.log_action(f"❌ Erreur lors du scan : {str(e)}", 'error') self.status_label.setText("Erreur lors du scan") print(f"ERREUR SCAN: {str(e)}") import traceback traceback.print_exc() # Sauvegarder même en cas d'erreur self.current_scan_active = False # Récupérer les logs depuis outlook_handler if hasattr(self.outlook_handler, 'scan_log_history') and len(self.outlook_handler.scan_log_history) > len( self.scan_log_history): print( f"RÉCUPÉRATION (erreur): Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler") self.scan_log_history = self.outlook_handler.scan_log_history.copy() try: print("SAUVEGARDE (erreur): Début sauvegarde d'urgence") result = self.auto_save_complete_scan_log() print(f"SAUVEGARDE (erreur): Résultat = {result}") except Exception as save_error: print(f"SAUVEGARDE (erreur): ❌ ERREUR CRITIQUE: {save_error}") finally: # === NETTOYAGE ET RÉINITIALISATION === print("FINALLY: Début du nettoyage") # Arrêter le timer if hasattr(self.main_window, 'stop_scan_timer'): self.main_window.stop_scan_timer() # Déconnecter les signaux try: self.outlook_handler.scan_progress.disconnect(self.update_progress) except: pass if hasattr(self.outlook_handler, 'cycle_status_update'): try: self.outlook_handler.cycle_status_update.disconnect(self.update_cycle_status) except: pass # Réinitialiser l'état de scanning self.is_scanning = False self.set_scanning_state(False) self.progress_bar.setValue(100) # *** IMPORTANT : LA SAUVEGARDE A DÉJÀ ÉTÉ FAITE AVANT CE POINT *** print("FINALLY: Nettoyage terminé - Journal déjà sauvegardé") print("=== FIN MÉTHODE START_SCAN ===") def _collect_links(self, mailbox_name, folder_names, sender, subject_keywords, link_keywords, max_emails): """ Récupère en différé tous les liens URL d'après les mêmes critères que start_scan. Version améliorée avec recherche récursive dans les dossiers Gmail. """ import re try: # Vérifier la connexion Outlook if not self.outlook_handler.outlook: # Forcer une reconnexion si nécessaire self.log_action("🔄 Vérification de la connexion Outlook...") # Tentative de reconnexion avec retry for attempt in range(3): if self.outlook_handler.connect(): break self.log_action(f"⚠️ Tentative de reconnexion {attempt + 1}/3...") import time time.sleep(2) else: self.log_action("❌ Impossible de se connecter à Outlook") return [] # 1) Connexion et accès aux dossiers self.log_action("📡 Accès à la boîte mail et aux dossiers...") mapi = self.outlook_handler.safe_rpc_call(lambda: self.outlook_handler.outlook) self.log_action("🔌 Namespace MAPI récupéré avec succès") if not mapi: self.log_action("❌ Impossible d'accéder au namespace MAPI") return [] self.log_action(f"📮 Tentative d'accès à la boîte mail : {mailbox_name}") root = self.outlook_handler.safe_rpc_call(lambda: mapi.Folders[mailbox_name]) if not root: self.log_action(f"❌ Impossible d'accéder à la boîte mail: {mailbox_name}") return [] # === DIAGNOSTIC APPROFONDI DES DOSSIERS === self.log_action("🔍 Diagnostic complet des dossiers disponibles...") all_folders_map = {} # Dictionnaire pour stocker tous les dossiers trouvés try: # Parcourir les dossiers principaux for i, folder_item in enumerate(root.Folders): folder_name = folder_item.Name all_folders_map[folder_name] = folder_item if i < 5: # Afficher seulement les 5 premiers dossiers principaux self.log_action(f"📁 Dossier principal : '{folder_name}'") # Explorer les sous-dossiers de tous les dossiers principaux try: sub_folders = list(folder_item.Folders) if len(sub_folders) > 0: self.log_action(f"📂 Exploration des sous-dossiers de '{folder_name}'...") for j, sub_folder in enumerate(sub_folders): sub_name = sub_folder.Name # Ajouter les sous-dossiers avec différentes stratégies de nommage full_path = f"{folder_name}/{sub_name}" all_folders_map[sub_name] = sub_folder # Nom simple all_folders_map[full_path] = sub_folder # Nom avec chemin complet if j < 6: # Afficher les 6 premiers sous-dossiers self.log_action(f" 📁 Sous-dossier : '{sub_name}'") if len(sub_folders) > 6: self.log_action(f" 📁 ... et {len(sub_folders) - 6} autres sous-dossiers") except Exception as subfolder_error: # Pas d'erreur critique si pas de sous-dossiers pass total_main = len(list(root.Folders)) if total_main > 5: self.log_action(f"📁 ... et {total_main - 5} autres dossiers principaux") except Exception as diag_error: self.log_action(f"❌ Erreur diagnostic général : {diag_error}") # === RECHERCHE ET ACCÈS AUX DOSSIERS DEMANDÉS === items = [] self.log_action(f"🎯 Recherche du dossier : '{folder_names[0]}'") for name in folder_names: clean_name = name.strip() found_folder = None # Stratégie de recherche multiple pour tous les fournisseurs search_attempts = [ clean_name, # Nom exact nettoyé name # Nom original (avec espaces éventuels) ] # Ajouter des tentatives avec les chemins des dossiers principaux for main_folder in ["[Gmail]", "INBOX", "Inbox", "Boîte de réception"]: if main_folder in all_folders_map: search_attempts.append(f"{main_folder}/{clean_name}") self.log_action(f"🔍 Recherche multi-fournisseur pour : '{clean_name}'") for attempt_name in search_attempts: if attempt_name in all_folders_map: found_folder = all_folders_map[attempt_name] self.log_action(f"✅ Dossier trouvé via : '{attempt_name}'") break # Si pas trouvé dans le mapping, essayer l'accès direct if not found_folder: try: self.log_action(f"🔄 Tentative accès direct : '{clean_name}'") found_folder = self.outlook_handler.safe_rpc_call(lambda: root.Folders[clean_name]) if found_folder: self.log_action(f"✅ Dossier trouvé par accès direct") except Exception as direct_error: self.log_action(f"❌ Échec accès direct : {direct_error}") # Traitement du dossier trouvé if found_folder: try: self.log_action(f"📂 Ouverture du dossier...") folder_items = self.outlook_handler.safe_rpc_call(lambda: found_folder.Items) if folder_items: folder_items_list = list(folder_items) items.extend(folder_items_list) self.log_action(f"📧 Collecté {len(folder_items_list)} emails du dossier") else: self.log_action(f"📭 Dossier vide") except Exception as folder_error: self.log_action(f"❌ Erreur lecture dossier : {folder_error}") continue else: # Affichage des dossiers disponibles pour aide self.log_action(f"❌ Dossier '{clean_name}' introuvable") self.log_action("💡 Dossiers disponibles contenant des mots-clés similaires :") # Recherche fuzzy pour suggestions suggestions = [] search_lower = clean_name.lower() for available_name in all_folders_map.keys(): if any(word in available_name.lower() for word in search_lower.split() if len(word) > 2): suggestions.append(available_name) if suggestions: for suggestion in suggestions[:3]: # Max 3 suggestions self.log_action(f" 💡 Suggestion : '{suggestion}'") else: self.log_action(" 💡 Aucune suggestion trouvée") if not items: self.log_action("❌ Aucun email trouvé dans les dossiers spécifiés") return [] self.log_action(f"📊 Total emails collectés pour analyse : {len(items)}") # === EXTRACTION DES LIENS AVEC FILTRAGE === self.log_action("🔗 Début de l'extraction des liens...") collected = [] count = 0 emails_processed = 0 emails_with_links = 0 for msg in items: if max_emails and emails_processed >= max_emails: self.log_action(f"⏹️ Limite de {max_emails} emails atteinte") break emails_processed += 1 try: # Vérifier les critères de l'email avant d'extraire les liens if not self.outlook_handler.should_process_message(msg, sender, subject_keywords): continue # Affichage de progression tous les 10 emails if emails_processed % 10 == 0: self.log_action(f"📈 Progression : {emails_processed}/{len(items)} emails analysés") # Extraire le corps du message de manière sécurisée body = '' try: if hasattr(msg, 'Body') and msg.Body: body = str(msg.Body) elif hasattr(msg, 'HTMLBody') and msg.HTMLBody: body = str(msg.HTMLBody) except Exception as e: self.log_action(f"⚠️ Erreur lecture corps email {emails_processed}: {str(e)}") continue if not body: continue # Nettoyer le corps et limiter la taille body = body[:10000] # Limiter à 10000 caractères # Compter les liens trouvés dans cet email links_in_this_email = 0 # Extraire tous les liens du corps for match in re.finditer(r'http[s]?://[^\s<>"\']+', body): try: url = match.group().strip().rstrip('<>"\'') # Filtrage par mots-clés si spécifiés if link_keywords: if not any(kw.lower() in url.lower() for kw in link_keywords): continue # Éviter les liens suspects exclude_keywords = ['delete', 'unsubscribe', 'remove', 'opt-out'] if any(ex_kw.lower() in url.lower() for ex_kw in exclude_keywords): continue # Éviter les doublons if url not in collected: collected.append(url) count += 1 links_in_this_email += 1 except Exception as e: self.log_action(f"⚠️ Erreur traitement lien: {str(e)}") continue # Compter les emails contenant au moins un lien if links_in_this_email > 0: emails_with_links += 1 except Exception as e: self.log_action(f"❌ Erreur traitement email {emails_processed}: {str(e)}") continue # === RAPPORT FINAL === self.log_action(f"✅ Extraction terminée :") self.log_action(f" 📊 {emails_processed} emails analysés") self.log_action(f" 📧 {emails_with_links} emails contenant des liens") self.log_action(f" 🔗 {count} liens uniques collectés") return collected except Exception as e: self.log_action(f"❌ Erreur globale dans _collect_links: {str(e)}") import traceback self.log_action(f"❌ Détails : {traceback.format_exc()[-200:]}") # Derniers 200 caractères return [] def start_scan_cached(self): """ Traitement différé des liens avec vérification HTTP+JS et garantie de sauvegarde complète des logs. Reprend la logique de start_scan avec validation 100% fiable des liens. """ import requests import psutil import os import json from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtCore import QUrl, QEventLoop, QTimer # === VÉRIFICATIONS PRÉLIMINAIRES === if not self.validate_inputs_for_scan(): self.log_action("❌ Validation des entrées pour le scan échouée") return link_keywords = self.link_entry.text().split() if hasattr(self, 'link_entry') and self.link_entry.text() else [] # === INITIALISATION DU SCAN ET CAPTURE DES LOGS === print("=== DÉBUT INITIALISATION SCAN CACHED ===") # Marquer le début du scan et initialiser l'historique self.current_scan_active = True self.scan_log_history = [] print(f"INIT: scan_log_history initialisé, taille: {len(self.scan_log_history)}") # Définir le type de journal self.set_log_type("Traitement Mail Cached") # Créer immédiatement le nom du journal avec horodatage from datetime import datetime current_time = datetime.now().strftime("%d/%m/%Y %H:%M") self.current_log_name = f"Traitement Mail Cached - {current_time}" print(f"INIT: Journal nommé: {self.current_log_name}") # === RÉINITIALISATION DES DONNÉES === self.report_data = [] self.deleted_emails = [] self.outlook_handler.links_clicked = 0 self.outlook_handler.links_skipped = 0 # Effacer le contenu du QTextEdit self.log_text.clear() self.log_action("💾 Nouveau traitement différé démarré", 'email') # === CONFIGURATION DU SYSTÈME DE LOG === self.log_categories = { 'email': {'color': '#98FB98', 'icon': '📧', 'enabled': True}, 'count': {'color': '#FFB347', 'icon': '🔢', 'enabled': True}, 'error': {'color': '#FF6B6B', 'icon': '❌', 'enabled': True}, 'success': {'color': '#4CAF50', 'icon': '✅', 'enabled': True}, 'system': {'color': '#87CEEB', 'icon': '⚙️', 'enabled': True} } # === INITIALISATION DE L'INTERFACE === self.is_scanning = True self.set_scanning_state(True) self.status_label.setText("Scan cached en cours...") self.progress_bar.setValue(0) try: # === RÉCUPÉRATION DES PARAMÈTRES === mailbox = self.mailbox_listbox.currentItem().text() folder = self.folder_listbox.currentItem().text() sender = self.sender_entry.text() subject_keywords = self.subject_entry.text().split() max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None # === PARTAGE CRITIQUE DE L'HISTORIQUE AVEC OUTLOOK_HANDLER === print(f"PARTAGE: Avant partage, scan_log_history taille: {len(self.scan_log_history)}") self.outlook_handler.scan_log_history = self.scan_log_history self.outlook_handler.current_scan_active = True print("PARTAGE: Historique partagé avec outlook_handler") # === PHASE 1 : COLLECTE DES LIENS === self.log_action("🗂️ Phase 1 : Collecte des liens depuis les emails...", 'system') # CORRECTION: Utiliser self._collect_links au lieu de self.outlook_handler._collect_links all_links = self._collect_links( mailbox, [folder], sender, subject_keywords, link_keywords, max_emails ) self.log_action(f"📊 Collecté {len(all_links)} liens au total", 'count') if not all_links: self.log_action("⚠️ Aucun lien trouvé dans les emails", 'error') self.current_scan_active = False self.auto_save_complete_scan_log() return # === PHASE 2 : VÉRIFICATION DES LIENS === self.log_action("🔍 Phase 2 : Vérification HTTP+JS des liens...", 'system') # Configuration mémoire (10% max) memory_info = psutil.virtual_memory() max_memory_bytes = memory_info.total * 0.1 # 10% de la RAM process = psutil.Process(os.getpid()) # Préparation du dossier fallback fallback_dir = r"C:\AUTOLOOK\Fallbackmemory" fallback_file = os.path.join(fallback_dir, "validated_links.json") try: os.makedirs(fallback_dir, exist_ok=True) self.log_action(f"📁 Dossier fallback créé/vérifié : {fallback_dir}", 'system') except Exception as e: self.log_action(f"❌ Erreur création dossier fallback : {e}", 'error') fallback_file = None # Initialisation d'une seule WebView web_view = QWebEngineView() web_view.setAttribute(Qt.WA_DontShowOnScreen, True) validated_links = [] risky_links = [] self.log_action("🚦 Début de la vérification individuelle des liens...", 'system') for i, link in enumerate(all_links): if not self.is_scanning: # Vérifier si l'utilisateur a arrêté break # Mise à jour du progrès progress = int((i / len(all_links)) * 50) # 50% pour la vérification self.progress_bar.setValue(progress) self.log_action(f"🔍 Vérification {i + 1}/{len(all_links)} : {link[:50]}...", 'system') # Vérifier la mémoire current_memory = process.memory_info().rss if current_memory > max_memory_bytes: self.log_action("⚠️ Seuil mémoire atteint, sauvegarde intermédiaire...", 'system') # Sauvegarder les liens validés dans le fichier JSON if fallback_file and validated_links: try: with open(fallback_file, 'w', encoding='utf-8') as f: json.dump(validated_links, f, indent=2, ensure_ascii=False) self.log_action(f"💾 {len(validated_links)} liens sauvegardés en JSON", 'success') validated_links.clear() # Vider la mémoire except Exception as e: self.log_action(f"❌ Erreur sauvegarde JSON : {e}", 'error') # === VÉRIFICATION HTTP === try: response = requests.head(link, timeout=10, allow_redirects=True) if response.status_code != 200: risky_links.append(f"{link} (HTTP {response.status_code})") self.log_action(f"⚠️ Lien HTTP non fiable : {response.status_code}", 'error') continue except Exception as e: risky_links.append(f"{link} (HTTP Error: {str(e)})") self.log_action(f"❌ Erreur HTTP : {str(e)[:30]}...", 'error') continue # === VÉRIFICATION JS === try: loop = QEventLoop() timeout_timer = QTimer() timeout_timer.timeout.connect(loop.quit) timeout_timer.setSingleShot(True) timeout_timer.start(15000) # 15 secondes max web_view.page().loadFinished.connect(loop.quit) web_view.page().load(QUrl(link)) loop.exec_() timeout_timer.stop() # Vérifier les redirections vraiment dangereuses uniquement final_url = web_view.page().url().toString() # Liste des domaines vraiment suspects (à adapter selon vos besoins) dangerous_domains = ['phishing', 'malware', 'virus', 'scam'] really_suspicious = any(danger in final_url.lower() for danger in dangerous_domains) if really_suspicious: risky_links.append(f"{link} (Redirection vers domaine suspect: {final_url})") self.log_action("⚠️ Redirection vers domaine dangereux détectée", 'error') continue # Log informatif de la redirection (sans bloquer) if final_url != link: self.log_action(f"🔄 Redirection: {link[:30]}... → {final_url[:30]}...", 'system') except Exception as e: risky_links.append(f"{link} (JS Error: {str(e)})") self.log_action(f"❌ Erreur JS : {str(e)[:30]}...", 'error') continue # Si on arrive ici, le lien est 100% fiable validated_links.append(link) self.log_action(f"✅ Lien validé avec succès", 'success') # Nettoyer la WebView web_view.deleteLater() # === RÉCUPÉRATION DES LIENS DEPUIS LE FICHIER JSON SI NÉCESSAIRE === if fallback_file and os.path.exists(fallback_file): try: with open(fallback_file, 'r', encoding='utf-8') as f: json_links = json.load(f) validated_links.extend(json_links) os.remove(fallback_file) # Nettoyer le fichier temporaire self.log_action(f"🔄 {len(json_links)} liens récupérés depuis JSON", 'success') except Exception as e: self.log_action(f"❌ Erreur lecture JSON : {e}", 'error') # === PHASE 3 : TRAITEMENT DES LIENS VALIDÉS === total_validated = len(validated_links) total_risky = len(risky_links) self.log_action(f"📊 Bilan vérification : {total_validated} fiables, {total_risky} à risque", 'count') if risky_links: self.log_action("⚠️ Liens écartés pour sécurité :", 'error') for risky in risky_links[:5]: # Afficher seulement les 5 premiers self.log_action(f" • {risky[:80]}...", 'error') if len(risky_links) > 5: self.log_action(f" • ... et {len(risky_links) - 5} autres", 'error') if not validated_links: self.log_action("❌ Aucun lien fiable à traiter", 'error') else: self.log_action(f"🚀 Phase 3 : Traitement de {total_validated} liens fiables...", 'system') # Traitement des liens validés for i, link in enumerate(validated_links): if not self.is_scanning: # Vérifier si l'utilisateur a arrêté break # Mise à jour du progrès (50-100%) progress = 50 + int((i / total_validated) * 50) self.progress_bar.setValue(progress) try: # CORRECTION: Utiliser une méthode de traitement des liens plus robuste if hasattr(self, '_process_validated_link'): self._process_validated_link(link, i + 1, total_validated) else: self.log_action(f"⚠️ Méthode _process_validated_link non trouvée", 'error') self.outlook_handler.links_skipped += 1 continue self.outlook_handler.links_clicked += 1 self.log_action(f"🖱️ Lien traité : {link[:50]}...", 'success') except Exception as e: self.outlook_handler.links_skipped += 1 self.log_action(f"⚠️ Échec traitement : {str(e)[:50]}...", 'error') # === TRAITEMENT IMMÉDIAT DE FIN DE SCAN === if self.is_scanning: self.log_action("🏁 Scan cached terminé avec succès", 'success') self.status_label.setText("Scan cached terminé.") self.count_label.setText(f"Liens traités: {self.outlook_handler.links_clicked}") self.update_reward_display() # *** POINT CRITIQUE : ARRÊTER LA CAPTURE ET SAUVEGARDER IMMÉDIATEMENT *** print("SAUVEGARDE: Arrêt de la capture des logs") self.current_scan_active = False # Récupérer les logs depuis outlook_handler si nécessaire if hasattr(self.outlook_handler, 'scan_log_history') and len( self.outlook_handler.scan_log_history) > len(self.scan_log_history): print( f"RÉCUPÉRATION: Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler") self.scan_log_history = self.outlook_handler.scan_log_history.copy() print(f"SAUVEGARDE: scan_log_history final contient {len(self.scan_log_history)} entrées") # Sauvegarder immédiatement AVANT tout reset try: print("SAUVEGARDE: Début sauvegarde immédiate") result = self.auto_save_complete_scan_log() print(f"SAUVEGARDE: Résultat = {result}") if result: print("SAUVEGARDE: ✅ SUCCÈS - Journal sauvegardé avec horodatage") else: print("SAUVEGARDE: ❌ ÉCHEC - Aucun fichier créé") except Exception as save_error: print(f"SAUVEGARDE: ❌ ERREUR: {save_error}") import traceback traceback.print_exc() else: self.log_action("⛔ Scan cached arrêté par l'utilisateur", 'system') self.status_label.setText("Scan cached arrêté.") # Même traitement pour scan arrêté self.current_scan_active = False # Récupérer les logs depuis outlook_handler if hasattr(self.outlook_handler, 'scan_log_history') and len( self.outlook_handler.scan_log_history) > len(self.scan_log_history): print( f"RÉCUPÉRATION (arrêt): Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler") self.scan_log_history = self.outlook_handler.scan_log_history.copy() # Sauvegarder même en cas d'arrêt try: print("SAUVEGARDE (arrêt): Début sauvegarde") result = self.auto_save_complete_scan_log() print(f"SAUVEGARDE (arrêt): Résultat = {result}") except Exception as save_error: print(f"SAUVEGARDE (arrêt): ❌ ERREUR: {save_error}") except Exception as e: # === GESTION DES ERREURS === self.log_action(f"❌ Erreur lors du scan cached : {str(e)}", 'error') self.status_label.setText("Erreur lors du scan cached") print(f"ERREUR SCAN CACHED: {str(e)}") import traceback traceback.print_exc() # Sauvegarder même en cas d'erreur self.current_scan_active = False # Récupérer les logs depuis outlook_handler if hasattr(self.outlook_handler, 'scan_log_history') and len(self.outlook_handler.scan_log_history) > len( self.scan_log_history): print( f"RÉCUPÉRATION (erreur): Copie {len(self.outlook_handler.scan_log_history)} logs depuis outlook_handler") self.scan_log_history = self.outlook_handler.scan_log_history.copy() try: print("SAUVEGARDE (erreur): Début sauvegarde d'urgence") result = self.auto_save_complete_scan_log() print(f"SAUVEGARDE (erreur): Résultat = {result}") except Exception as save_error: print(f"SAUVEGARDE (erreur): ❌ ERREUR CRITIQUE: {save_error}") finally: # === NETTOYAGE ET RÉINITIALISATION === print("FINALLY: Début du nettoyage cached") # Arrêter le timer if hasattr(self.main_window, 'stop_scan_timer'): self.main_window.stop_scan_timer() # Réinitialiser l'état de scanning self.is_scanning = False self.set_scanning_state(False) self.progress_bar.setValue(100) print("FINALLY: Nettoyage cached terminé - Journal déjà sauvegardé") print("=== FIN MÉTHODE START_SCAN_CACHED ===") def _process_validated_link(self, link, current_index, total_links): """ Version optimisée qui injecte du contenu au lieu de charger des URLs """ try: from datetime import datetime # Utiliser update_browser qui injecte maintenant du contenu self.update_browser(link) # Mise à jour de la console if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'console_header'): console = self.mini_browser.console_header console.show() console.update_page_time(f"Email {current_index}/{total_links}") # Enregistrer comme traité if hasattr(self.outlook_handler, 'duplicates_detection_enabled') and self.outlook_handler.duplicates_detection_enabled: if hasattr(self.outlook_handler, 'links_handler'): self.outlook_handler.links_handler.add_processed_link( link, metadata={ 'processed_date': datetime.now().isoformat(), 'method': 'content_injection' } ) self.outlook_handler.update_links_clicked() self.log_action(f"✅ Email traité: {link[:30]}...") except Exception as e: self.log_action(f"❌ Erreur traitement: {str(e)}") raise def start_scan_pattern(self): """Démarre un scan d'e-mails en mode pattern uniquement (analyse du corps du mail).""" # Validation des entrées pour le scan pattern if not self.mailbox_listbox.currentItem(): self.log_action("❌ Aucune boîte mail sélectionnée") return if not self.folder_listbox.selectedItems(): self.log_action("❌ Aucun dossier sélectionné") return # Réinitialiser les données self.report_data = [] self.deleted_emails = [] self.outlook_handler.links_clicked = 0 self.outlook_handler.links_skipped = 0 # Effacer le contenu des logs self.log_text.clear() self.log_action("🚀 Nouveau traitement démarré (mode PATTERN)") # Préparer l'interface self.is_scanning = True self.set_scanning_state(True) self.status_label.setText("Scan en cours...") self.progress_bar.setValue(0) try: # Récupérer les paramètres selected_mailbox = self.mailbox_listbox.currentItem() selected_folders = [item.text() for item in self.folder_listbox.selectedItems()] sender = self.sender_entry.text() subject_keywords = self.subject_entry.text().split() max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None self.log_action(f"📂 Boîte sélectionnée : {selected_mailbox.text()}") self.log_action(f"📁 Dossiers sélectionnés : {selected_folders}") # En mode pattern, utiliser les mots-clés prédéfinis depuis OutlookHandler link_keywords = self.outlook_handler.pattern_keywords if hasattr(self.outlook_handler, 'pattern_keywords') else [] self.log_action(f"🔍 Mots-clés (pattern) depuis fichier : {link_keywords}") # Si aucun mot-clé pattern n'est chargé, essayer de les recharger if not link_keywords: self.log_action("⚠️ Aucun mot-clé pattern trouvé, rechargement...") self.outlook_handler.load_pattern_keywords() link_keywords = self.outlook_handler.pattern_keywords self.log_action(f"🔄 Mots-clés rechargés : {link_keywords}") # Calcul de la durée estimée avg_pause_time = (self.outlook_handler.min_pause_time + self.outlook_handler.max_pause_time) / 2 avg_between_time = ( self.outlook_handler.min_between_pages_time + self.outlook_handler.max_between_pages_time) / 2 estimated_duration = max_emails * (avg_pause_time + avg_between_time) if max_emails else 0 self.remaining_time = int(estimated_duration) self.scan_timer.start(1000) # Connecter les signaux nécessaires self.outlook_handler.scan_progress.connect(self.update_progress) # Vérifier la connexion Outlook if not self.outlook_handler.ensure_connected(): self.log_action("❌ Connexion Outlook non établie") return # Initialiser le compteur self.outlook_handler.reset_scan_stats() # Lancer le scan en mode pattern clicked_links, matching_emails = self.outlook_handler.scan_emails_pattern( mailbox_name=selected_mailbox.text(), folder_names=selected_folders, sender=sender, subject_keywords=subject_keywords, link_keywords=link_keywords, max_emails=max_emails ) # Mise à jour de l'interface après scan if self.is_scanning: self.status_label.setText("Scan terminé.") self.count_label.setText(f"Nombre d'emails correspondants: {matching_emails}") self.update_reward_display() else: self.status_label.setText("Scan arrêté.") self.log_action("✅ Scan pattern terminé") except Exception as e: self.log_action(f"❌ Erreur lors du scan : {str(e)}") self.status_label.setText("Erreur lors du scan") finally: # Nettoyage final if hasattr(self.main_window, 'stop_scan_timer'): self.main_window.stop_scan_timer() try: self.outlook_handler.scan_progress.disconnect(self.update_progress) except: pass self.is_scanning = False self.set_scanning_state(False) self.progress_bar.setValue(100) def lock_windows_session(self): """Verrouille la session Windows en utilisant plusieurs méthodes.""" self.log_action("Tentative de verrouillage de la session Windows...") # Méthode 1: Utilisation directe de la commande Windows try: import subprocess self.log_action("Méthode 1: Utilisation de rundll32.exe directement") subprocess.call('rundll32.exe user32.dll,LockWorkStation') return # Si cette méthode fonctionne, on s'arrête là except Exception as e: self.log_action(f"Échec méthode 1: {str(e)}") # Méthode 2: Utilisation de ctypes try: import ctypes self.log_action("Méthode 2: Utilisation de ctypes.windll.user32.LockWorkStation") ctypes.windll.user32.LockWorkStation() return # Si cette méthode fonctionne, on s'arrête là except Exception as e: self.log_action(f"Échec méthode 2: {str(e)}") # Méthode 3: Utilisation d'un script VBS try: import tempfile import os self.log_action("Méthode 3: Utilisation d'un script VBS") # Créer un fichier temporaire pour le script VBS fd, path = tempfile.mkstemp(suffix='.vbs') try: with os.fdopen(fd, 'w') as f: f.write('Set WshShell = CreateObject("WScript.Shell")\n') f.write('WshShell.Run "rundll32.exe user32.dll,LockWorkStation", 0, False\n') # Exécuter le script avec une priorité élevée os.system(f'cscript "{path}"') self.log_action(f"Script VBS exécuté: {path}") finally: # Supprimer le fichier temporaire try: os.unlink(path) except Exception as e: self.log_action(f"Impossible de supprimer le fichier temporaire: {e}") except Exception as e: self.log_action(f"Échec méthode 3: {str(e)}") # Méthode 4: Utiliser une commande pour Windows 10/11 try: import os self.log_action("Méthode 4: Utilisation de la commande de verrouillage Windows 10/11") os.system('powershell -command "(New-Object -ComObject Shell.Application).WindowsSecurity()"') except Exception as e: self.log_action(f"Échec méthode 4: {str(e)}") def shutdown_windows(self): """Programme l'extinction de Windows.""" try: import os os.system('shutdown /s /t 30 /c "Extinction automatique programmée par l\'application."') self.log_action("Commande d'extinction exécutée avec délai de 30 secondes") except Exception as e: self.log_action(f"Erreur lors de la programmation de l'extinction: {str(e)}") def process_next_email(self): if not self.emails_to_process: self.finish_scan() return email = self.emails_to_process.pop(0) # Traiter l'email ici sender = email.SenderEmailAddress subject = email.Subject body = email.Body # Trouver un lien valide dans l'email link = self.outlook_handler.find_valid_link(email, self.link_keywords) if link: self.log_action(f"Traitement de l'email de {sender}: {subject}") self.update_browser(link) # Supprimer l'email après traitement try: if hasattr(self, 'permanent_delete_enabled') and hasattr(self, 'apply_to_scan') and self.permanent_delete_enabled and self.apply_to_scan: # Suppression définitive email.Delete(0) # Le paramètre 0 signifie suppression définitive self.log_action(f"Email supprimé définitivement : {subject}") else: # Suppression normale (vers la corbeille) email.Delete() self.log_action(f"Email déplacé vers la corbeille : {subject}") except Exception as e: self.log_action(f"Erreur lors de la suppression de l'email : {str(e)}") # Mettre à jour les compteurs self.outlook_handler.update_links_clicked() self.update_links_clicked_display(self.outlook_handler.links_clicked) # Programmer le traitement du prochain email QTimer.singleShot(self.outlook_handler.pause_time * 1000, self.process_next_email) else: self.log_action(f"Aucun lien valide trouvé dans l'email : {subject}") self.process_next_email() # Passer directement à l'email suivant def get_emails_to_process(self): emails = [] mailbox = self.mailbox_listbox.currentItem().text() folder = self.folder_listbox.currentItem().text() sender = self.sender_entry.text() subject_keywords = self.subject_entry.text().split() max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None try: outlook = self.outlook_handler.outlook root_folder = next((f for f in outlook.Folders if f.Name == mailbox), None) if not root_folder: raise Exception(f"Boîte mail '{mailbox}' non trouvée") target_folder = self.outlook_handler.find_folder(root_folder, folder) if not target_folder: raise Exception(f"Dossier '{folder}' non trouvé dans '{mailbox}'") messages = target_folder.Items messages.Sort("[ReceivedTime]", True) for message in list(messages): if self.outlook_handler.should_process_message(message, sender, subject_keywords): emails.append(message) if max_emails and len(emails) >= max_emails: break self.log_action(f"{len(emails)} emails trouvés pour traitement") return emails except Exception as e: self.log_action(f"Erreur lors de la récupération des emails : {str(e)}") return [] def _display_summary(self, summary_text): """Affiche le résumé avec un compte à rebours""" self.log_text.clear() self.log_text.append("=== RÉSUMÉ DU TRAITEMENT ===\n") self.log_text.append(summary_text) countdown = 5 # Durée en secondes self.summary_timer = QTimer(self) self.summary_countdown = countdown self.summary_timer.timeout.connect(self.update_summary_timer) self.summary_timer.start(1000) # Mise à jour chaque seconde def update_summary_timer(self): """Met à jour le compte à rebours du résumé""" self.summary_countdown -= 1 if self.summary_countdown > 0: # Mettre à jour uniquement la dernière ligne current_text = self.log_text.toPlainText() new_text = current_text.rsplit('\n', 1)[0] # self.log_text.setText(f"{new_text}\nCe résumé s'effacera dans {self.summary_countdown} secondes...") else: self.summary_timer.stop() self.log_text.clear() self.log_action("✅ Journal réinitialisé") def finish_scan(self): print("Début de finish_scan") try: # AJOUT : Marquer la fin du scan self.current_scan_active = False self.log_action("🏁 Scan terminé - Fin de l'enregistrement") # DOUBLE SAUVEGARDE pour sécurité maximale : # 1. Sauvegarder l'historique complet du scan (priorité) scan_file = self.auto_save_scan_with_timestamp() # 2. Sauvegarder aussi ce qui est visible (backup) visible_file = self.auto_save_log() # Log de confirmation if scan_file and visible_file: self.log_action("💾 Double sauvegarde effectuée (historique + visible)") elif scan_file: self.log_action("💾 Historique complet sauvegardé") elif visible_file: self.log_action("💾 Journal visible sauvegardé") # AJOUT : Marquer la fin du scan self.current_scan_active = False self.log_action("🏁 Scan terminé - Fin de l'enregistrement") # AJOUT : Sauvegarder automatiquement le journal complet du scan self.auto_save_scan_with_timestamp() self.is_scanning = False self.set_scanning_state(False) # Calculer les statistiques if hasattr(self, 'outlook_handler'): total_processed = self.outlook_handler.links_clicked total_skipped = self.outlook_handler.links_skipped total_reward = self.outlook_handler.total_reward else: total_processed = total_skipped = total_reward = 0 # Mettre à jour l'interface if hasattr(self, 'progress_bar'): self.progress_bar.setValue(100) if hasattr(self, 'status_label'): self.status_label.setText("Scan terminé") self.update_reward_display() # Vérifier les doublons (utiliser l'attribut de l'outlook_handler) doublons_count = getattr(self.outlook_handler, 'doublons_count', 0) if doublons_count > 0: logger.info(self._format_log_message(f"🔍 Total des doublons détectés : {doublons_count}", is_duplicate=False)) # Réinitialiser le compteur pour le prochain scan if hasattr(self.outlook_handler, 'doublons_count'): self.outlook_handler.doublons_count = 0 if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'bottom_banner'): banner = self.mini_browser.bottom_banner if hasattr(banner, 'setText'): banner.setText("En attente de traitement") elif hasattr(banner, 'show_banner'): banner.show_banner("En attente de traitement") banner.show() # NOUVEAU CODE: Rediriger les navigateurs vers une page d'accueil self.reset_browsers_to_homepage() # NOUVEAU CODE: Rediriger les navigateurs vers une page d'accueil self.reset_browsers_to_homepage() # Créer le résumé formaté summary = f""" RÉSUMÉ DU TRAITEMENT 📧 Emails traités : {total_processed:<15} ⏩ Emails ignorés : {total_skipped:<15} 🔍 Doublons détectés : {doublons_count:<15} 💰 Récompense : {total_reward:.2f} ⏱️ {QDateTime.currentDateTime().toString("dd/MM/yyyy HH:mm:ss")} """ # === PRÉSERVATION DE L'HISTORIQUE AVANT EFFACEMENT AUTOMATIQUE === try: # S'assurer que l'historique complet est sauvegardé AVANT l'effacement automatique if hasattr(self, 'scan_log_history') and self.scan_log_history: # Sauvegarder dans les journaux consultables if not hasattr(self, 'saved_journals'): self.saved_journals = {} # Créer une copie de l'historique pour consultation from datetime import datetime timestamp = datetime.now().strftime("%d/%m/%Y %H:%M") preservation_key = f"Traitement Mail - {timestamp}" # Convertir l'historique en HTML pour la consultation html_content = "" for entry in self.scan_log_history: html_content += f"
{entry}
" html_content += "" # Sauvegarder pour consultation via "Consulter journal" self.saved_journals[preservation_key] = html_content self.current_log_name = preservation_key print(f"DEBUG: Historique préservé pour consultation - {len(self.scan_log_history)} entrées") except Exception as e: print(f"ERREUR: Problème lors de la préservation de l'historique: {e}") # L'effacement automatique va avoir lieu, mais l'historique est préservé # Forcer la mise à jour immédiate du log et afficher le résumé # AJOUT OU MODIFICATION DE CETTE LIGNE : QTimer.singleShot(100, lambda: self.show_summary_with_timer(summary)) # Générer et afficher le rapport si demandé if hasattr(self.outlook_handler, 'main_window') and \ self.outlook_handler.main_window.report_toggle.isChecked(): report = self.generate_report() dialog = ReportDialog(report, self) dialog.exec_() self.start_log_clear_timer() except Exception as e: print(f"Erreur dans finish_scan: {e}") import traceback traceback.print_exc() finally: print("Fin de finish_scan") self.is_scanning = False self.set_scanning_state(False) def auto_save_scan_with_timestamp(self): """Sauvegarde automatique du journal de scan avec horodatage pour le menu déroulant""" try: from datetime import datetime # Créer un nom avec horodatage current_time = datetime.now() timestamp = current_time.strftime("%d/%m/%Y %H:%M") journal_name = f"Traitement Mail - {timestamp}" # Récupérer le contenu du journal complet du scan if hasattr(self, 'scan_log_history') and self.scan_log_history: # Convertir l'historique en HTML formaté html_content = "" for entry in self.scan_log_history: timestamp_str = entry.get('timestamp', '') message = entry.get('message', '') category = entry.get('category', 'system') # Récupérer la couleur selon la catégorie color = '#87CEEB' # couleur par défaut if hasattr(self, 'log_categories') and category in self.log_categories: color = self.log_categories[category]['color'] html_content += f"
{timestamp_str} {message}
" html_content += "" else: # Fallback : récupérer le contenu visible du journal html_content = self.log_text.toHtml() # Initialiser le stockage si nécessaire if not hasattr(self, 'saved_journals'): self.saved_journals = {} # Sauvegarder dans le dictionnaire des journaux self.saved_journals[journal_name] = html_content # Mettre à jour le nom du journal actuel self.current_log_name = journal_name # Log de confirmation self.log_action(f"💾 Journal sauvegardé : {journal_name}") return journal_name except Exception as e: self.log_action(f"❌ Erreur sauvegarde horodatée: {str(e)}") return None def stop_operation(self): if self.is_scanning: self.log_action("Arrêt du scan demandé") self.is_scanning = False self.outlook_handler.stop_scan() self.status_label.setText("Arrêt du scan en cours...") # AJOUT: Remettre le message par défaut sur la bannière if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'bottom_banner'): banner = self.mini_browser.bottom_banner if hasattr(banner, 'setText'): banner.setText("En attente de traitement") elif hasattr(banner, 'show_banner'): banner.show_banner("En attente de traitement") banner.show() def reset_browsers_to_homepage(self): """Redirige les navigateurs vers une page d'accueil après la fin du scan""" try: homepage_url = "https://www.snoown2klabs.com" # Page d'accueil par défaut (vide) # Rediriger le mini navigateur if hasattr(self, 'mini_browser') and self.mini_browser: mini_tab = self.mini_browser.get_current_tab() if mini_tab and hasattr(mini_tab, 'web_view'): mini_tab.web_view.load(QUrl(homepage_url)) self.log_action("Mini navigateur redirigé vers la page d'accueil") # MODIFICATION: S'assurer que la bannière affiche le message par défaut if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'bottom_banner'): banner = self.mini_browser.bottom_banner if hasattr(banner, 'setText'): banner.setText("En attente de traitement") elif hasattr(banner, 'show_banner'): banner.show_banner("En attente de traitement") banner.show() # Rediriger le navigateur principal s'il est visible if hasattr(self, 'browser_window') and self.browser_window and \ hasattr(self.browser_window, 'isVisible') and self.browser_window.isVisible(): main_tab = self.browser_window.get_current_tab() if main_tab and hasattr(main_tab, 'web_view'): main_tab.web_view.load(QUrl(homepage_url)) self.log_action("Navigateur principal redirigé vers la page d'accueil") # Effacer l'URL de la console if hasattr(self, 'mini_browser') and hasattr(self.mini_browser, 'console_header'): self.mini_browser.console_header.update_page_time("Prêt") if hasattr(self, 'browser_window') and hasattr(self.browser_window, 'console_header'): self.browser_window.console_header.update_page_time("Prêt") except Exception as e: self.log_action(f"Erreur lors de la redirection vers la page d'accueil: {str(e)}") def show_summary_with_timer(self, summary_text, duration_seconds=15): """ Affiche un résumé de fin de scan qui disparaît après le délai spécifié avec un compteur pour montrer le temps restant IMPORTANT : Cette méthode efface SEULEMENT l'affichage principal (log_text) et préserve tous les journaux sauvegardés accessibles via "Consulter journal" Args: summary_text (str): Le texte du résumé à afficher duration_seconds (int): Durée d'affichage en secondes """ try: # AVANT d'effacer l'affichage, s'assurer que l'historique est sauvegardé if hasattr(self, 'scan_log_history') and self.scan_log_history: print(f"DEBUG: Préservation de {len(self.scan_log_history)} entrées d'historique") # Effacer SEULEMENT le contenu de l'affichage principal self.log_text.clear() # Ajouter l'entête et le résumé self.log_text.append(f"
--- RÉSUMÉ DU SCAN ---
") self.log_text.append(summary_text) # Initialiser le compteur self.countdown_remaining = duration_seconds self.log_text.append( f"
Ce résumé disparaîtra dans {self.countdown_remaining} secondes...
") self.log_text.append( f"
💡 L'historique complet reste accessible via 'Consulter / Sauvegarder le journal'
") # Faire défiler vers le haut pour s'assurer que tout est visible self.log_text.verticalScrollBar().setValue(0) # Créer un timer pour le compte à rebours if hasattr(self, 'countdown_timer'): self.countdown_timer.stop() self.countdown_timer = QTimer(self) self.countdown_timer.timeout.connect(self.update_countdown) self.countdown_timer.start(1000) # Mise à jour chaque seconde # Créer un timer pour la réinitialisation finale if hasattr(self, 'summary_timer'): self.summary_timer.stop() self.summary_timer = QTimer(self) self.summary_timer.setSingleShot(True) self.summary_timer.timeout.connect(self.reset_log_after_summary) self.summary_timer.start(duration_seconds * 1000) # Forcer la mise à jour de l'interface QApplication.processEvents() except Exception as e: print(f"Erreur lors de l'affichage du résumé temporaire: {str(e)}") import traceback traceback.print_exc() # En cas d'erreur, forcer la réinitialisation du journal principal uniquement if hasattr(self, 'log_text'): self.log_text.clear() self.log_action("❌ Erreur lors de l'affichage du résumé - Affichage principal réinitialisé") def update_countdown(self): """Met à jour le compteur de temps restant dans le résumé SANS défilement""" try: # Décrémenter le compteur self.countdown_remaining -= 1 if self.countdown_remaining > 0: # Récupérer le contenu actuel text = self.log_text.toPlainText() lines = text.split('\n') # Remplacer seulement la dernière ligne si c'est un message de décompte if lines and "disparaîtra dans" in lines[-1]: content = '\n'.join(lines[:-1]) self.log_text.clear() self.log_text.append(content) # ✅ UTILISER la nouvelle méthode log_action avec auto_scroll=False self.log_action(f"Ce résumé disparaîtra dans {self.countdown_remaining} secondes...", auto_scroll=False) else: # Si pas de ligne de décompte existante, ajouter directement self.log_action(f"Ce résumé disparaîtra dans {self.countdown_remaining} secondes...", auto_scroll=False) else: # Arrêter le timer quand le compteur atteint zéro self.countdown_timer.stop() # ✅ NE PAS forcer la mise à jour pour éviter le défilement # QApplication.processEvents() # ← CETTE LIGNE EST COMMENTÉE/SUPPRIMÉE except Exception as e: print(f"Erreur lors de la mise à jour du compteur: {str(e)}") self.countdown_timer.stop() def reset_log_after_summary(self): """Réinitialise SEULEMENT l'affichage principal après le résumé automatique (fin de traitement)""" try: # S'assurer que les timers sont arrêtés if hasattr(self, 'summary_timer'): self.summary_timer.stop() if hasattr(self, 'countdown_timer'): self.countdown_timer.stop() # Effacer UNIQUEMENT l'affichage principal (log_text) # L'historique reste accessible via "Consulter journal" self.log_text.clear() # Message de confirmation que l'historique est préservé self.log_action("✅ Résumé terminé - Journal réinitialisé") self.log_action("💡 Historique complet disponible via 'Consulter / Sauvegarder le journal'") # Forcer la mise à jour de l'interface QApplication.processEvents() except Exception as e: print(f"Erreur lors de la réinitialisation du journal: {str(e)}") import traceback traceback.print_exc() # En cas d'erreur, effacer quand même l'affichage principal if hasattr(self, 'log_text'): self.log_text.clear() self.log_action(f"❌ Erreur de réinitialisation: {str(e)}") def create_widgets(self): """Méthode create_widgets() simplifiée pour widget enfant""" try: # === CONTENEUR PRINCIPAL SIMPLE === content_container = QWidget() content_layout = QVBoxLayout(content_container) content_layout.setContentsMargins(5, 5, 5, 5) content_layout.setSpacing(5) # === EN-TÊTE COMPACT === self._create_compact_header(content_layout) # === SPLITTER PRINCIPAL === content_splitter = QSplitter(Qt.Horizontal) content_splitter.setChildrenCollapsible(False) # === PANNEAU GAUCHE === left_panel = self.create_mail_and_folder_section() left_panel.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) content_splitter.addWidget(left_panel) # === PANNEAU CENTRAL === central_panel = self._create_simple_central_panel() content_splitter.addWidget(central_panel) # === PANNEAU DROIT === right_panel = self._create_simple_right_panel() content_splitter.addWidget(right_panel) # === PROPORTIONS SIMPLES === content_splitter.setSizes([300, 400, 400]) content_splitter.setStretchFactor(0, 1) content_splitter.setStretchFactor(1, 2) content_splitter.setStretchFactor(2, 2) # === ASSEMBLAGE === content_layout.addWidget(content_splitter) self.main_interface_layout.addWidget(content_container) # === INITIALISATION SIMPLE === if hasattr(self, 'connection_indicator') and hasattr(self, 'detected_connection_mode'): if self.detected_connection_mode: self.connection_indicator.set_mode(self.detected_connection_mode) self.log_action("✅ Interface widget enfant créée avec succès") except Exception as e: self.log_action(f"❌ Erreur création interface: {e}") def _create_compact_browser_controls(self): """Crée des contrôles de navigateur compacts""" try: controls_container = QWidget() controls_container.setFixedHeight(35) controls_layout = QHBoxLayout(controls_container) controls_layout.setContentsMargins(3, 3, 3, 3) controls_layout.setSpacing(5) # === ZOOM COMPACT === zoom_container = QWidget() zoom_layout = QHBoxLayout(zoom_container) zoom_layout.setContentsMargins(0, 0, 0, 0) zoom_layout.setSpacing(2) self.zoom_out_button = QPushButton("-") self.zoom_reset_button = QPushButton("🔄") self.zoom_in_button = QPushButton("+") button_style = """ QPushButton { background-color: #008CBA; color: white; border-radius: 2px; font-size: 11px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """ for button in [self.zoom_out_button, self.zoom_reset_button, self.zoom_in_button]: button.setFixedSize(20, 20) button.setStyleSheet(button_style) zoom_layout.addWidget(button) controls_layout.addWidget(zoom_container) # === TIMER COMPACT === self.timer_widget = TimerWidget(self) self.timer_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.timer_widget.set_text("----", "waiting") controls_layout.addWidget(self.timer_widget) # === BOUTON AGRANDIR COMPACT === self.toggle_browser_button = QPushButton("AGRANDIR") self.toggle_browser_button.setFixedSize(70, 20) self.toggle_browser_button.setStyleSheet(button_style) controls_layout.addWidget(self.toggle_browser_button) # Connecter les signaux self.zoom_in_button.clicked.connect(self.zoom_in) self.zoom_out_button.clicked.connect(self.zoom_out) self.zoom_reset_button.clicked.connect(self.reset_zoom) self.toggle_browser_button.clicked.connect(self.toggle_browser_view) return controls_container except Exception as e: self.log_action(f"❌ Erreur contrôles navigateur: {e}") return QWidget() def _create_compact_log_buttons(self, parent_layout): """Crée des boutons de journal compacts""" try: button_container = QWidget() button_container.setFixedHeight(30) button_layout = QHBoxLayout(button_container) button_layout.setContentsMargins(0, 0, 0, 0) button_layout.setSpacing(3) button_style = """ QPushButton { background-color: #3c4c5c; color: white; border-radius: 2px; font-size: 10px; padding: 2px; } QPushButton:hover { background-color: #4c5c6c; } """ self.toggle_log_button = QPushButton("Désactiver") self.toggle_log_button.setCheckable(True) self.toggle_log_button.setStyleSheet(button_style) self.toggle_log_button.clicked.connect(self.toggle_logging) button_layout.addWidget(self.toggle_log_button) self.view_log_button = QPushButton("Consulter") self.view_log_button.setStyleSheet(button_style + "color: #00ff00;") self.view_log_button.clicked.connect(self.show_log_dialog) button_layout.addWidget(self.view_log_button) self.clear_log_button = QPushButton("Effacer") self.clear_log_button.setStyleSheet(button_style) self.clear_log_button.clicked.connect(self.clear_log_text) button_layout.addWidget(self.clear_log_button) parent_layout.addWidget(button_container) except Exception as e: self.log_action(f"❌ Erreur boutons journal: {e}") def _optimize_section_layouts(self): """Optimise les layouts internes des sections""" try: sections = [ (self.search_criteria_group, "Critères"), (self.control_group, "Contrôles"), (self.progress_group, "Progression") ] for section, name in sections: if section and section.layout(): layout = section.layout() layout.setContentsMargins(4, 4, 4, 4) layout.setSpacing(2) self.log_action(f"🔧 Layout {name} optimisé") except Exception as e: self.log_action(f"❌ Erreur optimisation layouts: {e}") def _calculate_scale_factor(self, screen_width, screen_height): """Calcule un facteur d'échelle basé sur la résolution""" try: # Résolution de référence : 1920x1080 base_width = 1920 base_height = 1080 # Calculer les ratios width_ratio = screen_width / base_width height_ratio = screen_height / base_height # Prendre le ratio moyen, avec un minimum de 1.0 scale_factor = max(1.0, (width_ratio + height_ratio) / 2) self.log_action(f"📏 Facteur d'échelle calculé: {scale_factor:.2f}") return scale_factor except Exception as e: self.log_action(f"❌ Erreur calcul échelle: {e}") return 1.0 def _configure_splitter(self, splitter, screen_width, scale_factor): """Configure les proportions du splitter selon l'écran""" try: # Proportions selon la largeur d'écran if screen_width >= 3000: # Très haute résolution 4K+ sizes = [600, 900, 900] elif screen_width >= 2560: # 2K+ sizes = [500, 750, 750] elif screen_width >= 1920: sizes = [350, 500, 500] elif screen_width >= 1680: sizes = [300, 450, 450] elif screen_width >= 1366: sizes = [280, 400, 400] else: sizes = [250, 350, 350] splitter.setSizes(sizes) # Stretch factors équilibrés splitter.setStretchFactor(0, 1) # Gauche splitter.setStretchFactor(1, 2) # Centre (plus d'espace) splitter.setStretchFactor(2, 2) # Droite (plus d'espace) self.log_action(f"📐 Splitter configuré: {sizes}") except Exception as e: self.log_action(f"❌ Erreur configuration splitter: {e}") def _create_compact_header(self, parent_layout): """Crée un en-tête compact""" try: header_container = QWidget() header_container.setFixedHeight(40) header_layout = QHBoxLayout(header_container) header_layout.setContentsMargins(5, 2, 5, 2) header_layout.setSpacing(10) # Voyant de connexion self.connection_indicator = ConnectionIndicator() header_layout.addWidget(self.connection_indicator) # Titre centré self.title_label = QLabel("Tableau de bord : CREDITS") self.title_label.setFont(QFont("Arial", 12, QFont.Bold)) self.title_label.setAlignment(Qt.AlignCenter) self.title_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) header_layout.addWidget(self.title_label) # Bouton toggle panneau self.toggle_panel_button = QPushButton("≡") self.toggle_panel_button.setToolTip("Afficher/Masquer le panneau de sélection") self.toggle_panel_button.setFixedSize(25, 25) self.toggle_panel_button.setStyleSheet(""" QPushButton { background-color: #03699c; color: white; border-radius: 2px; font-size: 12px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """) self.toggle_panel_button.clicked.connect(self.toggle_mail_folder_panel) header_layout.addWidget(self.toggle_panel_button) parent_layout.addWidget(header_container) except Exception as e: self.log_action(f"❌ Erreur création en-tête: {e}") def _create_simple_central_panel(self): """Crée le panneau central avec des hauteurs fixes simples""" try: central_panel = QWidget() central_panel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) central_layout = QVBoxLayout(central_panel) central_layout.setContentsMargins(2, 2, 2, 2) central_layout.setSpacing(5) # === SECTION CRITÈRES (hauteur fixe) === self.search_criteria_group = self.create_search_criteria_section() self.search_criteria_group.setFixedHeight(180) central_layout.addWidget(self.search_criteria_group) # === SECTION CONTRÔLES (hauteur fixe) === self.control_group = self.create_control_section() self.control_group.setFixedHeight(120) central_layout.addWidget(self.control_group) # === SECTION PROGRESSION (extensible) === self.progress_group = self.create_progress_section() self.progress_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) central_layout.addWidget(self.progress_group, 1) # Prend l'espace restant return central_panel except Exception as e: self.log_action(f"❌ Erreur panneau central: {e}") return QWidget() def _create_simple_right_panel(self): """Crée le panneau droit avec des hauteurs simples""" try: right_panel = QWidget() right_panel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(2, 2, 2, 2) right_layout.setSpacing(5) # === MINI NAVIGATEUR === browser_group = QGroupBox("Mini Navigateur") browser_group.setFixedHeight(450) # Hauteur augmentée pour laisser plus de place au Timer browser_layout = QVBoxLayout(browser_group) browser_layout.setContentsMargins(3, 3, 3, 3) browser_layout.setSpacing(2) # Navigateur web self.mini_browser = BrowserWindow(is_mini=True) self.mini_browser.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) browser_layout.addWidget(self.mini_browser) # Contrôles du navigateur controls_container = self._create_compact_browser_controls() browser_layout.addWidget(controls_container) right_layout.addWidget(browser_group) # === JOURNAL === log_group = QGroupBox("Journal") log_group.setFixedHeight(150) # Hauteur fixe réduite pour le journal log_layout = QVBoxLayout(log_group) log_layout.setContentsMargins(3, 3, 3, 3) log_layout.setSpacing(2) # Boutons du journal self._create_compact_log_buttons(log_layout) # Zone de texte du journal self.log_text = QTextEdit() self.log_text.setReadOnly(True) self.log_text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) log_layout.addWidget(self.log_text) right_layout.addWidget(log_group) return right_panel except Exception as e: self.log_action(f"❌ Erreur panneau droit: {e}") return QWidget() def setup_responsive_system(self): """Initialise le système de responsivité avancé""" try: # Timer pour gérer les redimensionnements self.resize_timer = QTimer() self.resize_timer.setSingleShot(True) self.resize_timer.timeout.connect(self.on_delayed_resize) # Stocker la taille initiale self.previous_size = self.size() self.log_action("🎯 Système de responsivité avancé initialisé") except Exception as e: self.log_action(f"❌ Erreur init responsivité: {e}") def on_delayed_resize(self): """Gère le redimensionnement avec délai""" try: # Vérifier à nouveau les débordements après redimensionnement QTimer.singleShot(50, self.check_initial_overflow) except Exception as e: self.log_action(f"❌ Erreur redimensionnement retardé: {e}") def handle_window_resize(self, event): """ Gère le redimensionnement de la fenêtre pour maintenir les proportions et adapter l'interface selon le mode (fenêtré/plein écran) """ try: # Vérifier que l'événement est valide if not hasattr(event, 'size'): return from PyQt5.QtWidgets import QApplication new_size = event.size() is_fullscreen = new_size.width() > 1600 and new_size.height() > 900 # Reste du code inchangé... # === AJUSTEMENT DES PROPORTIONS DU SPLITTER === if hasattr(self, 'content_splitter'): current_sizes = self.content_splitter.sizes() total_width = sum(current_sizes) if is_fullscreen: # Mode plein écran : donner plus d'espace au panneau droit (navigateur) new_sizes = [ int(total_width * 0.20), # 20% pour le panneau gauche int(total_width * 0.35), # 35% pour le panneau central int(total_width * 0.45) # 45% pour le panneau droit ] else: # Mode fenêtré : proportions équilibrées new_sizes = [ int(total_width * 0.30), # 30% pour le panneau gauche int(total_width * 0.35), # 35% pour le panneau central int(total_width * 0.35) # 35% pour le panneau droit ] self.content_splitter.setSizes(new_sizes) # Force la mise à jour de l'affichage QApplication.processEvents() self.log_action(f"🔄 Interface redimensionnée : {new_size.width()}x{new_size.height()}") except Exception as e: self.log_action(f"❌ Erreur redimensionnement: {e}") def adapt_font_sizes_to_window(self, window_size): """ Adapte la taille des polices en fonction de la taille de la fenêtre """ try: # Calculer un facteur d'échelle basé sur la largeur base_width = 1200 # Largeur de référence scale_factor = window_size.width() / base_width scale_factor = max(0.8, min(scale_factor, 1.5)) # Limiter entre 80% et 150% # Ajuster la taille des polices principales if hasattr(self, 'title_label'): font = self.title_label.font() base_size = 16 new_size = max(12, int(base_size * scale_factor)) font.setPointSize(new_size) self.title_label.setFont(font) # Adapter les autres éléments de l'interface if hasattr(self, 'dashboard_label'): font = self.dashboard_label.font() base_size = 12 new_size = max(10, int(base_size * scale_factor)) font.setPointSize(new_size) self.dashboard_label.setFont(font) except Exception as e: self.log_action(f"❌ Erreur adaptation polices: {e}") def toggle_fullscreen_mode(self): """ Bascule entre le mode fenêtré et plein écran avec adaptation complète de l'interface """ try: from PyQt5.QtCore import QTimer if hasattr(self, 'main_window'): was_fullscreen = self.main_window.isFullScreen() if was_fullscreen: self.main_window.showNormal() self.log_action("🔳 Mode fenêtré activé") # Forcer l'adaptation immédiate au mode fenêtré QTimer.singleShot(200, self.adapt_to_windowed_mode) else: self.main_window.showFullScreen() self.log_action("🔲 Mode plein écran activé") # Forcer l'adaptation immédiate au mode plein écran QTimer.singleShot(200, self.adapt_to_fullscreen_mode) except Exception as e: self.log_action(f"❌ Erreur basculement plein écran: {e}") def adapt_to_fullscreen_mode(self): """ Adapte spécifiquement l'interface au mode plein écran """ try: from PyQt5.QtWidgets import QDesktopWidget from PyQt5.QtCore import QTimer screen = QDesktopWidget().availableGeometry() screen_height = screen.height() # === ADAPTATION DU MINI-NAVIGATEUR === if hasattr(self, 'mini_browser'): min_height = int(screen_height * 0.35) max_height = int(screen_height * 0.65) self.mini_browser.setMinimumHeight(min_height) self.mini_browser.setMaximumHeight(max_height) # === ADAPTATION DES SECTIONS CENTRALES === if hasattr(self, 'search_criteria_group'): self.search_criteria_group.setMinimumHeight(int(screen_height * 0.25)) self.search_criteria_group.setMaximumHeight(16777215) if hasattr(self, 'control_group'): self.control_group.setMinimumHeight(int(screen_height * 0.20)) self.control_group.setMaximumHeight(16777215) if hasattr(self, 'progress_group'): self.progress_group.setMinimumHeight(int(screen_height * 0.35)) self.progress_group.setMaximumHeight(16777215) # === ADAPTATION DU JOURNAL === if hasattr(self, 'log_text'): self.log_text.setMinimumHeight(int(screen_height * 0.15)) # === AJUSTEMENT DES PROPORTIONS DU SPLITTER === if hasattr(self, 'content_splitter'): QTimer.singleShot(100, self.adjust_splitter_for_fullscreen) self.log_action("✨ Interface adaptée au mode plein écran") except Exception as e: self.log_action(f"❌ Erreur adaptation plein écran: {e}") def adapt_to_windowed_mode(self): """ Adapte spécifiquement l'interface au mode fenêtré """ try: from PyQt5.QtCore import QTimer # === RETOUR AUX TAILLES STANDARD === if hasattr(self, 'mini_browser'): self.mini_browser.setMinimumHeight(200) self.mini_browser.setMaximumHeight(400) if hasattr(self, 'search_criteria_group'): self.search_criteria_group.setMinimumHeight(180) if hasattr(self, 'control_group'): self.control_group.setMinimumHeight(160) if hasattr(self, 'progress_group'): self.progress_group.setMinimumHeight(350) if hasattr(self, 'log_text'): self.log_text.setMinimumHeight(100) # === AJUSTEMENT DES PROPORTIONS DU SPLITTER === if hasattr(self, 'content_splitter'): QTimer.singleShot(100, self.adjust_splitter_for_windowed) self.log_action("📐 Interface adaptée au mode fenêtré") except Exception as e: self.log_action(f"❌ Erreur adaptation fenêtré: {e}") def adjust_splitter_for_fullscreen(self): """Ajuste les proportions du splitter pour le plein écran""" try: if hasattr(self, 'content_splitter'): current_sizes = self.content_splitter.sizes() total_width = sum(current_sizes) new_sizes = [ int(total_width * 0.20), # 20% panneau gauche int(total_width * 0.35), # 35% panneau central int(total_width * 0.45) # 45% panneau droit ] self.content_splitter.setSizes(new_sizes) except Exception as e: self.log_action(f"❌ Erreur ajustement splitter plein écran: {e}") def adjust_splitter_for_windowed(self): """Ajuste les proportions du splitter pour le mode fenêtré""" try: if hasattr(self, 'content_splitter'): current_sizes = self.content_splitter.sizes() total_width = sum(current_sizes) new_sizes = [ int(total_width * 0.30), # 30% panneau gauche int(total_width * 0.35), # 35% panneau central int(total_width * 0.35) # 35% panneau droit ] self.content_splitter.setSizes(new_sizes) except Exception as e: self.log_action(f"❌ Erreur ajustement splitter fenêtré: {e}") def show_log_dialog(self): """Boîte de dialogue journal avec filtres dynamiques""" from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QComboBox, QLineEdit, QTextEdit, QPushButton, \ QCheckBox, QLabel from PyQt5.QtCore import Qt from PyQt5.QtGui import QFont dialog = QDialog(self) dialog.setWindowTitle("Journal d'Événements - Affichage Dynamique") dialog.setMinimumSize(900, 700) layout = QVBoxLayout(dialog) # === EN-TÊTE AVEC SÉLECTION DE JOURNAL === header = QHBoxLayout() # Sélection du journal précédent header.addWidget(QLabel("Journal:")) self.journal_combo = QComboBox() self._load_previous_journals() # Mettre à jour le menu avec le journal actuel si il existe if hasattr(self, 'current_log_name') and self.current_log_name: # Chercher et sélectionner le journal actuel dans le menu for i in range(self.journal_combo.count()): item_text = self.journal_combo.itemText(i) if self.current_log_name in item_text: self.journal_combo.setCurrentIndex(i) break self.journal_combo.currentTextChanged.connect(self._on_journal_changed) header.addWidget(self.journal_combo) # Nom du journal actuel (modifiable) header.addWidget(QLabel("Nom:")) self.name_edit = QLineEdit(getattr(self, 'current_log_name', 'Journal actuel')) self.name_edit.textChanged.connect(self._on_name_changed) header.addWidget(self.name_edit) layout.addLayout(header) # === FILTRES DYNAMIQUES === filters = QHBoxLayout() filters.addWidget(QLabel("Afficher:")) # Initialiser les catégories de log si elles n'existent pas if not hasattr(self, 'log_categories'): self.log_categories = { 'email': {'color': '#98FB98', 'icon': '📧', 'enabled': True}, 'count': {'color': '#FFB347', 'icon': '🔢', 'enabled': True}, 'error': {'color': '#FF6B6B', 'icon': '❌', 'enabled': True}, 'success': {'color': '#4CAF50', 'icon': '✅', 'enabled': True}, 'info': {'color': '#87CEEB', 'icon': 'ℹ️', 'enabled': True}, 'system': {'color': '#87CEEB', 'icon': '⚙️', 'enabled': True} } self.dialog_checkboxes = {} for cat_key, cat_info in self.log_categories.items(): checkbox = QCheckBox(f"{cat_info['icon']} {cat_key.title()}") checkbox.setChecked(cat_info['enabled']) checkbox.setStyleSheet(f"color: {cat_info['color']}; font-weight: bold;") # CONNEXION DYNAMIQUE checkbox.toggled.connect(lambda checked, key=cat_key: self._filter_log_dynamically(key, checked)) self.dialog_checkboxes[cat_key] = checkbox filters.addWidget(checkbox) # Boutons de filtrage rapide all_btn = QPushButton("Tout") all_btn.clicked.connect(self._select_all_categories) filters.addWidget(all_btn) none_btn = QPushButton("Rien") none_btn.clicked.connect(self._deselect_all_categories) filters.addWidget(none_btn) errors_btn = QPushButton("Erreurs") errors_btn.clicked.connect(self._show_only_errors) filters.addWidget(errors_btn) layout.addLayout(filters) # === ZONE DE TEXTE DU JOURNAL === self.dialog_log_display = QTextEdit() self.dialog_log_display.setReadOnly(True) self.dialog_log_display.setFont(QFont("Consolas", 9)) # Charger le contenu initial self._refresh_dialog_display() layout.addWidget(self.dialog_log_display) # === STATISTIQUES === self.stats_label = QLabel() self._update_stats_display() layout.addWidget(self.stats_label) # === BOUTONS D'ACTION === buttons = QHBoxLayout() save_btn = QPushButton("💾 Sauvegarder Journal") save_btn.clicked.connect(self._save_current_journal) buttons.addWidget(save_btn) export_btn = QPushButton("📤 Exporter Filtré") export_btn.clicked.connect(self._export_filtered_log) buttons.addWidget(export_btn) clear_btn = QPushButton("🗑️ Vider") clear_btn.clicked.connect(self._clear_current_log) buttons.addWidget(clear_btn) buttons.addStretch() close_btn = QPushButton("❌ Fermer") close_btn.clicked.connect(dialog.accept) buttons.addWidget(close_btn) layout.addLayout(buttons) dialog.exec_() def _load_previous_journals(self): """Charge la liste des journaux précédents triés par date""" try: # Initialiser le stockage des journaux si nécessaire if not hasattr(self, 'saved_journals'): self.saved_journals = {} # Vider le combo box self.journal_combo.clear() # Ajouter le journal actuel en premier self.journal_combo.addItem("📝 Journal Actuel") # Trier les journaux par ordre chronologique décroissant (plus récent en premier) sorted_journals = [] for journal_name in self.saved_journals.keys(): try: # Extraire la date du nom du journal if " - " in journal_name: date_part = journal_name.split(" - ", 1)[1] from datetime import datetime date_obj = datetime.strptime(date_part, "%d/%m/%Y %H:%M") sorted_journals.append((date_obj, journal_name)) else: # Pour les anciens journaux sans format de date sorted_journals.append((datetime.min, journal_name)) except: # En cas d'erreur de parsing, mettre à la fin from datetime import datetime sorted_journals.append((datetime.min, journal_name)) # Trier par date décroissante sorted_journals.sort(key=lambda x: x[0], reverse=True) # Ajouter les journaux triés for date_obj, journal_name in sorted_journals: self.journal_combo.addItem(f"📋 {journal_name}") # Log du nombre de journaux chargés self.log_action(f"📚 {len(sorted_journals)} journaux précédents chargés") except Exception as e: self.log_action(f"❌ Erreur chargement journaux: {str(e)}") def _on_journal_changed(self, selected_name): """Met à jour l'affichage quand l'utilisateur sélectionne un journal""" try: if hasattr(self, 'saved_journals') and selected_name in self.saved_journals: html = self.saved_journals[selected_name] self.dialog_log_display.setHtml(html) else: self.dialog_log_display.clear() # Met à jour aussi la zone de saisie du nom self.name_edit.setText(selected_name) # Réinitialise les filtres visuels selon le nouveau contenu self._refresh_dialog_display() self._update_stats_display() except Exception as e: print(f"❌ Erreur lors du changement de journal : {e}") def _on_name_changed(self, new_name): """Gère le changement de nom du journal""" self.current_log_name = new_name def _filter_log_dynamically(self, category, enabled): """VERSION ULTRA-SIMPLE - Reconstruction complète""" try: # Mettre à jour le filtre self.log_categories[category]['enabled'] = enabled # NOUVELLE APPROCHE : Reconstruire ligne par ligne self.dialog_log_display.clear() # Récupérer le contenu texte ligne par ligne du journal principal original_text = self.log_text.toPlainText() for line in original_text.split('\n'): if line.strip(): # Ignorer les lignes vides # Vérifier si cette ligne doit être affichée should_show = True # Pour chaque catégorie désactivée for cat_key, cat_info in self.log_categories.items(): if not cat_info['enabled'] and cat_info['icon'] in line: should_show = False break if should_show: # Reconstituer le HTML avec la bonne couleur line_category = self._detect_line_category(line) if line_category: cat_info = self.log_categories[line_category] colored_line = f'
{line}
' self.dialog_log_display.append(colored_line) else: # Ligne sans catégorie détectée self.dialog_log_display.append( f'
{line}
') except Exception as e: print(f"ERREUR: {e}") def _detect_line_category(self, line): """Détecte la catégorie d'une ligne""" for cat_key, cat_info in self.log_categories.items(): if cat_info['icon'] in line: return cat_key return None def _refresh_dialog_display(self): """Recharge le journal avec priorité sur l'historique sauvegardé""" try: # Priorité à l'historique sauvegardé même si l'affichage principal est vide if hasattr(self, 'current_log_name') and hasattr(self, 'saved_journals'): if self.current_log_name in self.saved_journals: self.dialog_log_display.setHtml(self.saved_journals[self.current_log_name]) return # Sinon utiliser l'historique du scan si disponible if hasattr(self, 'scan_log_history') and self.scan_log_history: html_content = "" for entry in self.scan_log_history: html_content += f"
{entry}
" html_content += "" self.dialog_log_display.setHtml(html_content) return # En dernier recours, utiliser le contenu visible self.dialog_log_display.setHtml(self.log_text.toHtml()) except Exception as e: print(f"Erreur refresh: {e}") def _update_stats_display(self): """Met à jour l'affichage des statistiques""" try: if not hasattr(self, 'stats_label'): return # Compter les messages par catégorie content = self.dialog_log_display.toPlainText() stats = {} total = 0 for cat_key, cat_info in self.log_categories.items(): count = content.count(cat_info['icon']) stats[cat_key] = count if cat_info['enabled']: total += count # Créer le texte des statistiques stats_text = f"Total affiché: {total} | " for cat_key, count in stats.items(): if count > 0: icon = self.log_categories[cat_key]['icon'] stats_text += f"{icon}{count} " self.stats_label.setText(stats_text) self.stats_label.setStyleSheet("color: #65ddfb; font-weight: bold; padding: 5px;") except Exception as e: print(f"Erreur stats: {e}") def _select_all_categories(self): """Sélectionne toutes les catégories""" for checkbox in self.dialog_checkboxes.values(): checkbox.setChecked(True) def _deselect_all_categories(self): """Désélectionne toutes les catégories""" for checkbox in self.dialog_checkboxes.values(): checkbox.setChecked(False) def _show_only_errors(self): """Affiche uniquement les erreurs et avertissements""" for cat_key, checkbox in self.dialog_checkboxes.items(): should_check = cat_key in ['error', 'system'] checkbox.setChecked(should_check) def _save_current_journal(self): """Sauvegarde le journal actuel avec horodatage, dans la mémoire interne et la liste déroulante""" try: from PyQt5.QtWidgets import QMessageBox from PyQt5.QtCore import QDateTime if not hasattr(self, 'log_text') or not self.log_text: return # Initialiser la structure de stockage si nécessaire if not hasattr(self, 'saved_journals'): self.saved_journals = {} # Obtenir le contenu HTML actuel current_content = self.log_text.toHtml() if not current_content.strip(): return # Ne rien sauvegarder si le journal est vide # Générer un nom horodaté lisible timestamp = QDateTime.currentDateTime().toString("dd/MM HH:mm") self.current_log_name = f"Journal - {timestamp}" # Sauvegarder dans le dictionnaire mémoire self.saved_journals[self.current_log_name] = current_content # Ajouter à la liste déroulante s'il n'existe pas déjà existing_items = [self.journal_combo.itemText(i) for i in range(self.journal_combo.count())] if self.current_log_name not in existing_items: self.journal_combo.addItem(self.current_log_name) # Sélectionner automatiquement ce journal self.journal_combo.setCurrentText(self.current_log_name) QMessageBox.information(self.parent(), "Sauvegardé", f"Journal '{self.current_log_name}' sauvegardé avec succès") except Exception as e: print(f"❌ Erreur lors de la sauvegarde du journal : {e}") def _export_filtered_log(self): """Exporte le journal filtré""" try: from PyQt5.QtWidgets import QFileDialog, QMessageBox from PyQt5.QtCore import QDateTime content = self.dialog_log_display.toPlainText() filename = f"{self.current_log_name}_{QDateTime.currentDateTime().toString('yyyyMMdd_HHmm')}.txt" filepath, _ = QFileDialog.getSaveFileName(self.parent(), "Exporter Journal Filtré", filename, "Fichiers texte (*.txt)") if filepath: with open(filepath, 'w', encoding='utf-8') as f: f.write(f"=== {self.current_log_name} ===\n") f.write(f"Exporté le: {QDateTime.currentDateTime().toString('dd/MM/yyyy HH:mm')}\n\n") # Ajouter les filtres actifs active_filters = [cat_key for cat_key, cat_info in self.log_categories.items() if cat_info['enabled']] f.write(f"Filtres actifs: {', '.join(active_filters)}\n") f.write("=" * 50 + "\n\n") f.write(content) QMessageBox.information(self.parent(), "Exporté", f"Journal exporté vers:\n{filepath}") except Exception as e: from PyQt5.QtWidgets import QMessageBox QMessageBox.warning(self.parent(), "Erreur", f"Erreur export: {e}") def _clear_current_log(self): """Vide le journal actuel""" try: from PyQt5.QtWidgets import QMessageBox reply = QMessageBox.question(self.parent(), "Confirmer", "Êtes-vous sûr de vouloir vider le journal actuel ?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: self.log_text.clear() self.dialog_log_display.clear() self._update_stats_display() except Exception as e: print(f"Erreur clear log: {e}") def clear_log_text(self): """Efface le contenu du journal""" try: self.log_text.clear() self.log_action("✅ Journal effacé manuellement") except Exception as e: print(f"Erreur clear log text: {e}") # Méthodes nécessaires pour le fonctionnement des boutons de zoom et navigateur def zoom_in(self): """Augmente le zoom du mini navigateur""" try: if hasattr(self, 'mini_browser'): current_tab = self.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): current_zoom = current_tab.web_view.zoomFactor() new_zoom = min(current_zoom + 0.1, 2.0) # Maximum zoom 200% current_tab.web_view.setZoomFactor(new_zoom) zoom_percentage = int(new_zoom * 100) self.log_action(f"🔍 Zoom augmenté à {zoom_percentage}%") except Exception as e: self.log_action(f"❌ Erreur zoom avant: {str(e)}") def zoom_out(self): """Diminue le zoom du mini navigateur""" try: if hasattr(self, 'mini_browser'): current_tab = self.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): current_zoom = current_tab.web_view.zoomFactor() new_zoom = max(current_zoom - 0.1, 0.3) # Minimum zoom 30% current_tab.web_view.setZoomFactor(new_zoom) zoom_percentage = int(new_zoom * 100) self.log_action(f"🔍 Zoom réduit à {zoom_percentage}%") except Exception as e: self.log_action(f"❌ Erreur zoom arrière: {str(e)}") def reset_zoom(self): """Réinitialise le zoom du mini navigateur à 30%""" try: if hasattr(self, 'mini_browser'): current_tab = self.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): default_zoom = 0.3 current_tab.web_view.setZoomFactor(default_zoom) zoom_percentage = int(default_zoom * 100) self.log_action(f"🔄 Zoom réinitialisé à {zoom_percentage}%") except Exception as e: self.log_action(f"❌ Erreur réinitialisation zoom: {str(e)}") def create_mail_and_folder_section(self): self.mail_folder_container = QWidget() container_layout = QVBoxLayout(self.mail_folder_container) container_layout.setContentsMargins(0, 0, 0, 0) group = QGroupBox("Sélection boite mails et dossiers") group.setMinimumHeight(420) layout = QVBoxLayout(group) layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(2) # --- Styles --- list_style = """QListWidget { background-color: #03699c; border: 1px solid #dcdcdc; border-radius: 4px; font-size: 14px; color: white; } QListWidget::item { padding: 1px; height: 14px; color: white; } QListWidget::item:selected { background-color: #0078D7; }""" input_style = """QLineEdit { background-color: #03699c; color: white; border: 1px solid #dcdcdc; border-radius: 4px; padding: 5px; font-size: 12px; min-height: 25px; }""" label_style = """QLabel { color: white; font-size: 12px; padding-bottom: 2px; }""" button_style = """QPushButton { background-color: #3ea6a3; color: white; border: none; border-radius: 2px; padding: 1px 2px; font-size: 9px; max-width: 45px; min-height: 18px; } QPushButton:hover { background-color: #007B9A; }""" placeholder_style = """QLineEdit { background-color: #03699c; color: white; border: 1px solid #dcdcdc; border-radius: 4px; padding: 5px; } QLineEdit::placeholder { color: rgba(255, 255, 255, 0.7); font-style: italic; }""" layout.addSpacing(15) # --- Boîtes mail --- self.mailbox_listbox = QListWidget() self.mailbox_listbox.setMinimumWidth(250) self.mailbox_listbox.setFixedHeight(100) self.mailbox_listbox.setStyleSheet(list_style) self.mailbox_listbox.itemClicked.connect(self.on_mailbox_selected) layout.addWidget(self.mailbox_listbox) layout.addSpacing(15) # --- Dossiers --- self.folder_listbox = QListWidget() self.folder_listbox.setStyleSheet(list_style) self.folder_listbox.setFixedHeight(100) layout.addWidget(self.folder_listbox) # --- Barre de recherche --- search_container = QWidget() search_layout = QVBoxLayout(search_container) search_layout.setContentsMargins(0, 0, 0, 0) search_layout.setSpacing(0) # Optionnel pour réduire l’espace self.folder_search = QLineEdit() self.folder_search.setStyleSheet(input_style) self.folder_search.setFixedHeight(25) self.folder_search.setMaximumWidth(350) self.folder_search.setPlaceholderText("🔍 Rechercher un dossier...") self.folder_search.textChanged.connect(self.filter_folders) search_layout.addWidget(self.folder_search) layout.addWidget(search_container) # --- Boutons de contrôle --- button_container = QWidget() button_layout = QHBoxLayout(button_container) button_layout.setContentsMargins(0, 0, 0, 0) button_layout.setSpacing(1) buttons = [ ("Déplier", self.expand_all_folders), ("Replier", self.collapse_all_folders), ("Actual.", lambda: self.update_folders( self.mailbox_listbox.currentItem().text() if self.mailbox_listbox.currentItem() else None)), ("Stats", self.show_folder_stats) ] for text, callback in buttons: button = QPushButton(text) button.setFixedHeight(18) button.setStyleSheet(button_style) button.clicked.connect(callback) button_layout.addWidget(button) layout.addWidget(button_container) # --- Champs de filtrage (dans une grille) --- self.link_mode = "url" self.sender_entry = QLineEdit() self.subject_entry = QLineEdit() self.link_entry = QLineEdit() self.reward_entry = QLineEdit() self.sender_entry.setPlaceholderText("Expéditeur") self.subject_entry.setPlaceholderText("Mots-clés du titre") self.link_entry.setPlaceholderText("Mots-clés du lien") self.reward_entry.setPlaceholderText("Récompense") for field in [self.sender_entry, self.subject_entry, self.link_entry, self.reward_entry]: field.setStyleSheet(placeholder_style) # Bouton toggle mode URL / Pattern self.link_mode_toggle = QPushButton("Keyword Mode : Manuel") self.link_mode_toggle.setCheckable(True) self.link_mode_toggle.setChecked(False) self.link_mode_toggle.clicked.connect(self.toggle_link_mode) self.link_mode_toggle.setStyleSheet(""" QPushButton { background-color: #28a745; color: white; border-radius: 4px; padding: 5px; font-weight: bold; } """) # Grille des champs form_grid_container = QWidget() form_grid_layout = QGridLayout(form_grid_container) form_grid_layout.setSpacing(5) form_grid_layout.setContentsMargins(0, 0, 0, 0) # Ligne 0 : champ Expéditeur en pleine largeur form_grid_layout.addWidget(self.sender_entry, 0, 0, 1, 2) # Ligne 1 : Mot-clé du titre et Récompense côte à côte form_grid_layout.addWidget(self.subject_entry, 1, 0) form_grid_layout.addWidget(self.reward_entry, 1, 1) # Ligne 2 : Toggle URL/Pattern et champ du lien form_grid_layout.addWidget(self.link_mode_toggle, 2, 0) form_grid_layout.addWidget(self.link_entry, 2, 1) # Étirement égal des colonnes form_grid_layout.setColumnStretch(0, 1) form_grid_layout.setColumnStretch(1, 1) # Ajout au layout principal layout.addWidget(form_grid_container) # --- Boutons de récompense --- reward_buttons_container = QWidget() reward_buttons_layout = QHBoxLayout(reward_buttons_container) reward_buttons_layout.setContentsMargins(5, 15, 5, 15) reward_buttons_layout.setSpacing(5) self.validate_reward_button = QPushButton("Valider récompense") self.reset_reward_button = QPushButton("RAZ récompense") reward_button_style = """ QPushButton { background-color: #00bab5; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-weight: bold; font-size: 12px; min-height: 20px; } QPushButton:hover { background-color: #009994; } QPushButton:disabled { background-color: #cccccc; color: #666666; } """ self.validate_reward_button.setStyleSheet(reward_button_style) self.reset_reward_button.setStyleSheet(reward_button_style) self.validate_reward_button.clicked.connect(self.validate_reward) self.reset_reward_button.clicked.connect(self.reset_reward) reward_buttons_layout.addWidget(self.validate_reward_button) reward_buttons_layout.addWidget(self.reset_reward_button) layout.addWidget(reward_buttons_container) container_layout.addWidget(group) # Timer masquage panneau self.panel_timer = QTimer(self) self.panel_timer.setSingleShot(True) self.panel_timer.timeout.connect(self.hide_mail_folder_panel) return self.mail_folder_container def create_control_section(self): group = QGroupBox("Contrôles") group.setMaximumHeight(180) group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) layout = QVBoxLayout(group) layout.setSpacing(10) # Première ligne de boutons top_buttons_container = QWidget() top_buttons_layout = QHBoxLayout(top_buttons_container) top_buttons_layout.setContentsMargins(0, 0, 0, 0) top_buttons_layout.setSpacing(5) self.reset_reload_button = QPushButton("Réinitialiser") self.reset_reload_button.setToolTip("Réinitialiser tous les critères de recherche") self.reset_reload_button.clicked.connect(self.reset_search_criteria) self.stop_button = QPushButton("Arrêter") self.pause_button = QPushButton("Pause") self.skip_button = QPushButton("Passer") buttons_first_row = [self.reset_reload_button, self.stop_button, self.pause_button, self.skip_button] for button in buttons_first_row: button.setStyleSheet(self.button_style) button.setFixedHeight(30) top_buttons_layout.addWidget(button) layout.addWidget(top_buttons_container) # Deuxième ligne : scan, nettoyer, planificateur buttons_container = QWidget() buttons_layout = QHBoxLayout(buttons_container) buttons_layout.setContentsMargins(0, 0, 0, 0) buttons_layout.setSpacing(5) self.start_button = QPushButton("Gagner Crédits") self.count_button = QPushButton("Nettoyage Mails") self.scheduler_button = QPushButton("Planificateur") for btn in [self.start_button, self.count_button, self.scheduler_button]: btn.setStyleSheet(self.button_style) btn.setFixedHeight(30) buttons_layout.addWidget(btn) layout.addWidget(buttons_container) # Animation comptage self.count_timer = QTimer(self) self.count_timer.timeout.connect(self.update_count_button_text) self.count_animation_step = 0 # États initiaux self.stop_button.setEnabled(False) self.pause_button.setEnabled(False) self.skip_button.setEnabled(False) # Connexions self.start_button.clicked.connect(self.on_start_button_clicked) self.stop_button.clicked.connect(self.stop_operation) self.pause_button.clicked.connect(self.toggle_pause) self.skip_button.clicked.connect(self.skip_current_email) self.count_button.clicked.connect(self.cleanup_emails) self.scheduler_button.clicked.connect(self.open_scheduler) return group def setup_fullscreen_shortcut(self): """ Configure le raccourci clavier pour le plein écran (F11) """ try: from PyQt5.QtWidgets import QShortcut from PyQt5.QtGui import QKeySequence fullscreen_shortcut = QShortcut(QKeySequence("F11"), self) fullscreen_shortcut.activated.connect(self.toggle_fullscreen_mode) self.log_action("⌨️ Raccourci F11 configuré pour le plein écran") except Exception as e: self.log_action(f"❌ Erreur configuration raccourci: {e}") def set_log_type(self, log_type): """Change le type de journal""" self.current_log_type = log_type timestamp = QDateTime.currentDateTime().toString("dd/MM HH:mm") self.current_log_name = f"{log_type} - {timestamp}" def _toggle_category(self, category, enabled): """Active/désactive une catégorie""" self.log_categories[category]['enabled'] = enabled def _save_simple_log(self, content): """Sauvegarde simple du journal""" filename = f"{self.current_log_name}_{QDateTime.currentDateTime().toString('yyyyMMdd_HHmm')}.txt" filepath, _ = QFileDialog.getSaveFileName(self, "Sauvegarder", filename, "Fichiers texte (*.txt)") if filepath: with open(filepath, 'w', encoding='utf-8') as f: f.write(f"=== {self.current_log_name} ===\n") f.write(f"Généré le: {QDateTime.currentDateTime().toString('dd/MM/yyyy HH:mm')}\n\n") f.write(content) QMessageBox.information(self, "Sauvegardé", f"Journal sauvegardé dans:\n{filepath}") def save_log_to_file(self, log_content, log_type="journal"): """ Sauvegarde le contenu du journal dans un fichier .txt """ try: if not log_content.strip(): QMessageBox.warning(self, "Attention", "Le journal sélectionné est vide.") return timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') default_filename = f"{log_type}_{timestamp}.txt" default_path = os.path.join(LOG_DIR, default_filename) file_path, _ = QFileDialog.getSaveFileName( self, f"Sauvegarder {log_type}", default_path, "Fichiers texte (*.txt);;Tous les fichiers (*.*)" ) if file_path: with open(file_path, "w", encoding="utf-8") as f: # Ajouter un en-tête informatif f.write(f"=== {log_type.upper().replace('_', ' ')} ===\n") f.write(f"Généré le: {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}\n") f.write(f"Nombre d'entrées: {len(log_content.split(chr(10)))}\n") f.write("=" * 50 + "\n\n") f.write(log_content) f.write(f"\n\n=== FIN {log_type.upper().replace('_', ' ')} ===") QMessageBox.information(self, "Succès", f"Journal sauvegardé :\n{file_path}") self.log_action(f"💾 {log_type} sauvegardé dans: {os.path.basename(file_path)}") except Exception as e: QMessageBox.critical(self, "Erreur", f"Erreur lors de la sauvegarde : {e}") self.log_action(f"❌ Erreur sauvegarde: {str(e)}") def auto_save_complete_scan_log(self): """ Sauvegarde automatique de l'HISTORIQUE COMPLET du dernier scan - Sur disque (fichier texte) - Dans le menu déroulant (format HTML) """ try: # === DEBUG INITIAL === print(f"DEBUG: auto_save_complete_scan_log appelée") print(f"DEBUG: scan_log_history existe = {hasattr(self, 'scan_log_history')}") print(f"DEBUG: scan_log_history taille = {len(getattr(self, 'scan_log_history', []))}") # === VÉRIFICATION DE L'HISTORIQUE === if not hasattr(self, 'scan_log_history') or not self.scan_log_history: print("ERREUR: scan_log_history vide ou inexistant") # Tentative de récupération depuis outlook_handler if hasattr(self, 'outlook_handler') and hasattr(self.outlook_handler, 'scan_log_history'): print(f"RÉCUPÉRATION: outlook_handler a {len(self.outlook_handler.scan_log_history)} entrées") self.scan_log_history = self.outlook_handler.scan_log_history.copy() else: print("ÉCHEC: Aucun historique disponible nulle part") self.log_action("⚠️ Aucun historique de scan à sauvegarder") return None # === IMPORTS NÉCESSAIRES === from datetime import datetime import os # === SAUVEGARDE SUR DISQUE === print("SAUVEGARDE: Début sauvegarde sur disque") timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"scan_complet_{timestamp}.txt" # Définir LOG_DIR si pas défini if not hasattr(self, 'LOG_DIR'): LOG_DIR = os.path.join(os.path.expanduser("~"), "Documents", "OutlookLogs") os.makedirs(LOG_DIR, exist_ok=True) else: LOG_DIR = self.LOG_DIR filepath = os.path.join(LOG_DIR, filename) with open(filepath, "w", encoding="utf-8") as f: f.write("=== HISTORIQUE COMPLET DU SCAN ===\n") f.write(f"Généré le: {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}\n") f.write(f"Nombre d'entrées: {len(self.scan_log_history)}\n") f.write("Note: Ceci contient TOUS les logs du scan, même ceux effacés de l'affichage\n") f.write("=" * 60 + "\n\n") for entry in self.scan_log_history: f.write(f"{entry}\n") f.write(f"\n\n=== FIN HISTORIQUE COMPLET ({len(self.scan_log_history)} entrées) ===") print(f"SAUVEGARDE: Fichier créé: {filepath}") # === SAUVEGARDE POUR LE MENU DÉROULANT === print("SAUVEGARDE: Début sauvegarde pour menu déroulant") # Créer un nom avec horodatage pour le menu display_timestamp = datetime.now().strftime("%d/%m/%Y %H:%M") journal_name = f"Traitement Mail - {display_timestamp}" print(f"SAUVEGARDE: Nom du journal: {journal_name}") # Convertir l'historique en HTML formaté pour l'affichage html_content = "" for entry in self.scan_log_history: # Analyser chaque entrée pour extraire le timestamp et le message entry_str = str(entry) # Déterminer la couleur selon le contenu du message color = '#87CEEB' # couleur par défaut (system) if '📧' in entry_str or 'Email' in entry_str or 'email' in entry_str.lower(): color = '#98FB98' # vert clair pour email elif '🔢' in entry_str or 'Compteur' in entry_str or 'traités' in entry_str: color = '#FFB347' # orange pour count elif '❌' in entry_str or 'Erreur' in entry_str or 'erreur' in entry_str.lower(): color = '#FF6B6B' # rouge pour error elif '✅' in entry_str or 'Scan terminé' in entry_str or 'succès' in entry_str.lower() or 'terminé' in entry_str.lower(): color = '#4CAF50' # vert pour success elif '🔗' in entry_str or 'lien' in entry_str.lower() or 'cliqué' in entry_str.lower(): color = '#FFA500' # orange pour liens elif '🏁' in entry_str or 'fin' in entry_str.lower(): color = '#32CD32' # vert lime pour fin elif '🚀' in entry_str or 'début' in entry_str.lower(): color = '#00BFFF' # bleu ciel pour début elif '📡' in entry_str or 'connexion' in entry_str.lower(): color = '#4169E1' # bleu royal pour connexion elif '⚙️' in entry_str or 'Configuration' in entry_str or 'system' in entry_str.lower(): color = '#87CEEB' # bleu pour system html_content += f"
{entry_str}
" html_content += "" # Initialiser le stockage si nécessaire if not hasattr(self, 'saved_journals'): self.saved_journals = {} print("SAUVEGARDE: Initialisation saved_journals") # Sauvegarder dans le dictionnaire des journaux pour le menu déroulant self.saved_journals[journal_name] = html_content print(f"SAUVEGARDE: Journal ajouté à saved_journals, total: {len(self.saved_journals)}") # Mettre à jour le nom du journal actuel self.current_log_name = journal_name print(f"SAUVEGARDE: current_log_name mis à jour: {journal_name}") # Log de confirmation self.log_action(f"💾 Historique complet sauvegardé: {filename}") self.log_action(f"📋 Journal ajouté au menu: {journal_name}") print(f"SAUVEGARDE: Succès complet, retour: {filepath}") return filepath except Exception as e: print(f"ERREUR SAUVEGARDE: {str(e)}") import traceback traceback.print_exc() # Essayer de log l'erreur si possible try: self.log_action(f"❌ Erreur sauvegarde historique complet: {str(e)}") except: print("Impossible de logger l'erreur") return None def cleanup_old_log_files(self, days_to_keep=7): """ Nettoie les anciens fichiers de log (garde seulement les X derniers jours) """ try: if not os.path.exists(LOG_DIR): return current_time = time.time() cutoff_time = current_time - (days_to_keep * 24 * 60 * 60) cleaned_count = 0 for filename in os.listdir(LOG_DIR): if filename.startswith(('log_auto_', 'journal_visible_', 'scan_complet_')): filepath = os.path.join(LOG_DIR, filename) if os.path.getctime(filepath) < cutoff_time: os.remove(filepath) cleaned_count += 1 if cleaned_count > 0: self.log_action(f"🧹 {cleaned_count} anciens fichiers de log supprimés") except Exception as e: self.log_action(f"❌ Erreur nettoyage logs: {str(e)}") def clear_complete_history(self): """ Vide l'historique complet après confirmation """ reply = QMessageBox.question( self, "Confirmer", "Êtes-vous sûr de vouloir vider tout l'historique des logs ?\n\nCette action est irréversible.", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: if hasattr(self, 'complete_log_history'): self.complete_log_history.clear() if hasattr(self, 'scan_log_history'): self.scan_log_history.clear() if hasattr(self, 'log_text'): self.log_text.clear() # Effacer tous les QTextEdit de la boîte si visible for widget in QApplication.allWidgets(): if isinstance(widget, QDialog) and widget.windowTitle() == "Journal des événements": text_edits = widget.findChildren(QTextEdit) for edit in text_edits: edit.clear() # 🔁 Mettre à jour les noms des onglets tab_widget = widget.findChild(QTabWidget) if tab_widget: tab_widget.setTabText(0, "📺 Actuel (0)") tab_widget.setTabText(1, "📚 Complet (0)") tab_widget.setTabText(2, "🎯 Scan (0)") self.log_action("🗑️ Historique des logs vidé") QMessageBox.information(self, "Confirmé", "Historique des logs vidé avec succès.") def auto_save_log(self): """ Sauvegarde automatique du journal VISIBLE dans un fichier daté. Complète la sauvegarde de l'historique complet du scan. """ try: if hasattr(self, 'log_text'): content = self.log_text.toPlainText() if content.strip(): # Ne pas sauvegarder si vide timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') filename = f"journal_visible_{timestamp}.txt" filepath = os.path.join(LOG_DIR, filename) with open(filepath, "w", encoding="utf-8") as f: # En-tête pour distinguer du journal complet f.write("=== JOURNAL VISIBLE AU MOMENT DE LA SAUVEGARDE ===\n") f.write(f"Généré le: {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}\n") f.write("Note: Ceci est le contenu visible à l'écran, pas l'historique complet\n") f.write("=" * 60 + "\n\n") f.write(content) f.write("\n\n=== FIN DU JOURNAL VISIBLE ===") # Message dans l'interface (plus discret) self.log_action(f"💾 Journal visible sauvegardé: {filename}") return filepath # Retourner le chemin pour usage externe si besoin except Exception as e: self.log_action(f"❌ Erreur sauvegarde journal visible: {str(e)}") return None def start_log_clear_timer(self): self.log_clear_remaining = 0 self.log_clear_timer.start(1000) self.log_action("⏳ Timer journal démarré (1 minutes)") def update_log_timer(self): if self.log_clear_remaining > 0: self.log_clear_remaining -= 1 # Remplacer la dernière ligne au lieu d'en ajouter une nouvelle current_text = self.log_text.toPlainText() lines = current_text.split('\n') if lines and "Effacement journal dans" in lines[-1]: lines[ -1] = f"{QDateTime.currentDateTime().toString('HH:mm:ss')} - ⏳ Effacement journal dans {self.log_clear_remaining} s" self.log_text.setPlainText('\n'.join(lines)) else: self.log_action(f"⏳ Effacement journal dans {self.log_clear_remaining} s") else: self.log_clear_timer.stop() self.log_text.clear() self.log_action("✅ Journal effacé automatiquement après 2 minutes") def resizeEvent(self, event): """Gère les événements de redimensionnement avec protection débordement""" try: super().resizeEvent(event) # Démarrer le timer pour traiter le redimensionnement après un délai if hasattr(self, 'resize_timer'): self.resize_timer.start(200) # 200ms de délai except Exception as e: print(f"Erreur dans resizeEvent: {e}") def set_styles(self): self.input_style = """ QLineEdit, QListWidget { background-color: #03699c; color: white; border: 1px solid #dcdcdc; border-radius: 4px; padding: 5px; } """ self.button_style = """ QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; font-size: 10px; } QPushButton:hover { background-color: #007B9A; } QPushButton:disabled { background-color: #cccccc; color: #666666; } """ def toggle_link_mode(self): """ Bascule entre le mode URL et Pattern, avec changement de couleur et visibilité du champ. """ if self.link_mode_toggle.isChecked(): self.link_mode_toggle.setText("Keyword Mode : Automatique") self.link_entry.setVisible(False) self.link_mode = "pattern" self.link_mode_toggle.setStyleSheet(""" QPushButton { background-color: #6f42c1; color: white; border-radius: 4px; padding: 5px; font-weight: bold; } """) else: self.link_mode_toggle.setText("Keyword Mode : Manuel") self.link_entry.setVisible(True) self.link_mode = "url" self.link_mode_toggle.setStyleSheet(""" QPushButton { background-color: #28a745; color: white; border-radius: 4px; padding: 5px; font-weight: bold; } """) def toggle_mail_folder_panel(self): """Version alternative - Redimensionnement forcé""" try: if self.mail_folder_container.isVisible(): # MASQUER panel_width = self.mail_folder_container.width() self.mail_folder_container.setVisible(False) # Trouver la fenêtre principale par remontée widget = self while widget.parent(): widget = widget.parent() main_window = widget # Réduction forcée current_size = main_window.size() new_width = current_size.width() - panel_width new_width = 850 main_window.setFixedWidth(new_width) # Forcer la largeur # Puis remettre en redimensionnable QTimer.singleShot(100, lambda: main_window.setMinimumWidth(0)) QTimer.singleShot(100, lambda: main_window.setMaximumWidth(16777215)) self.toggle_panel_button.setText("☰") self.stored_panel_width = panel_width else: # AFFICHER self.mail_folder_container.setVisible(True) # Restauration forcée widget = self while widget.parent(): widget = widget.parent() main_window = widget panel_width = getattr(self, 'stored_panel_width', 400) current_size = main_window.size() new_width = current_size.width() + panel_width main_window.setFixedWidth(new_width) # Puis remettre en redimensionnable QTimer.singleShot(100, lambda: main_window.setMinimumWidth(0)) QTimer.singleShot(100, lambda: main_window.setMaximumWidth(16777215)) self.toggle_panel_button.setText("✕") QApplication.processEvents() except Exception as e: print(f"Erreur: {e}") def show_mail_folder_panel(self): """Affiche le panneau de sélection et démarre le timer si spécifié""" self.mail_folder_container.setVisible(True) # Arrêter le timer s'il est en cours if self.panel_timer.isActive(): self.panel_timer.stop() def hide_mail_folder_panel(self): """Masque le panneau de sélection""" self.mail_folder_container.setVisible(False) def filter_folders(self, text): """Filtre les dossiers en fonction du texte de recherche""" for i in range(self.folder_listbox.count()): item = self.folder_listbox.item(i) item.setHidden(text.lower() not in item.text().lower()) def update_folder_tree(self, folders_data): """Met à jour l'arbre des dossiers avec les nouvelles données""" self.folder_tree.clear() def add_folder_item(parent, folder_info): item = QTreeWidgetItem(parent) item.setText(0, folder_info['name']) item.setText(1, str(folder_info['total_messages'])) item.setText(2, str(folder_info['unread_messages'])) item.setData(0, Qt.UserRole, folder_info['id']) if folder_info.get('children'): for child in folder_info['children']: add_folder_item(item, child) for folder in folders_data: add_folder_item(self.folder_tree, folder) self.folder_tree.expandAll() for i in range(self.folder_tree.columnCount()): self.folder_tree.resizeColumnToContents(i) def create_search_criteria_section(self): group = QGroupBox("Critères de recherche") group.setMaximumHeight(200) # Réduire considérablement la hauteur group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) layout = QVBoxLayout(group) layout.setSpacing(5) # Style commun pour les inputs input_style = """ QLineEdit { background-color: #03699c; color: white; border: 1px solid #dcdcdc; border-radius: 4px; padding: 5px; font-size: 12px; } """ # Container pour la ligne principale main_row = QWidget() main_layout = QHBoxLayout(main_row) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(5) # Espacement entre les éléments # 1. Container pour le nombre max d'emails (avec label + champ) max_emails_container = QWidget() max_emails_layout = QHBoxLayout(max_emails_container) max_emails_layout.setContentsMargins(0, 0, 0, 0) max_emails_layout.setSpacing(5) max_emails_label = QLabel("Nbre Emails:") max_emails_layout.addWidget(max_emails_label) self.max_emails_entry = QLineEdit() self.max_emails_entry.setFixedWidth(60) self.max_emails_entry.setFixedHeight(25) self.max_emails_entry.setMaxLength(4) self.max_emails_entry.setAlignment(Qt.AlignCenter) self.max_emails_entry.setStyleSheet(input_style) max_emails_layout.addWidget(self.max_emails_entry) main_layout.addWidget(max_emails_container) # 3. Container pour l'authentification (label + checkbox) auth_container = QWidget() auth_layout = QHBoxLayout(auth_container) auth_layout.setContentsMargins(0, 0, 0, 0) auth_layout.setSpacing(5) auth_label = QLabel("Authentification:") self.auth_checkbox = QCheckBox() auth_layout.addWidget(auth_label) auth_layout.addWidget(self.auth_checkbox) main_layout.addWidget(auth_container) # 2. Container pour la validité (label + checkbox + champ de saisie sur la même ligne) validity_container = QWidget() validity_layout = QHBoxLayout(validity_container) validity_layout.setContentsMargins(0, 0, 0, 0) validity_layout.setSpacing(5) validity_label = QLabel("Date de validité:") self.validity_checkbox = QCheckBox() self.validity_entry = QLineEdit() self.validity_entry.setFixedWidth(60) self.validity_entry.setFixedHeight(25) self.validity_entry.setMaxLength(3) self.validity_entry.setAlignment(Qt.AlignCenter) self.validity_entry.setStyleSheet(input_style) self.validity_entry.setVisible(False) # Caché par défaut validity_layout.addWidget(validity_label) validity_layout.addWidget(self.validity_checkbox) validity_layout.addWidget(self.validity_entry) main_layout.addWidget(validity_container) # Ajouter un extensible pour pousser les éléments vers la gauche main_layout.addStretch() # Ajouter la ligne principale au layout layout.addWidget(main_row) # Ajouter un second espace pour descendre davantage le scrolltext spacer2 = QWidget() spacer2.setFixedHeight(15) layout.addWidget(spacer2) # Ajouter la ligne principale au layout layout.addWidget(main_row) # Ajouter un second espace pour descendre davantage le scrolltext spacer2 = QWidget() spacer2.setFixedHeight(15) layout.addWidget(spacer2) # Création du texte défilant pour les astuces avec un conteneur fixe tips_container = QWidget() tips_container.setFixedHeight(40) # Hauteur fixe augmentée tips_layout = QVBoxLayout(tips_container) tips_layout.setContentsMargins(0, 0, 0, 0) self.tips_label = QLabel() self.tips_label.setStyleSheet(""" QLabel { background-color: #2c3e50; color: #42effb; padding: 5px; border-radius: 3px; font-size: 12px; font-weight: bold; } """) self.tips_label.setFixedHeight(30) # Légèrement plus haut self.tips_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) tips_layout.addWidget(self.tips_label) # Ajouter le conteneur au layout principal layout.addWidget(tips_container) # Initialiser l'animation du texte défilant self.setup_scrolling_tips() layout.addWidget(spacer2) # Créer un bouton à trois états pour le contrôle des conseils self.tips_control_button = QPushButton("Cliquez pour changer de mode: Mode Tips Défilant") self.tips_control_button.setStyleSheet(""" QPushButton { background-color: #6b6bf4; color: white; border: none; padding: 5px 8px; border-radius: 4px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #007B9A; } """) # État initial: défilant (1) settings = QSettings("Snoown2K", "MA2C") self.tips_mode = settings.value("tips_mode", 1, type=int) # 0 = désactivé, 1 = défilant, 2 = aléatoire avec fondu self.apply_tips_mode(self.tips_mode) self.tips_control_button.clicked.connect(self.toggle_tips_mode) layout.addWidget(self.tips_control_button) # Connecter le checkbox de validité pour afficher/masquer le champ de validité self.validity_checkbox.stateChanged.connect( lambda state: self.validity_entry.setVisible(state == Qt.Checked) ) # Connecter le signal de la checkbox d'authentification self.auth_checkbox.stateChanged.connect(self.toggle_auth_interface) # Ajouter un espace extensible à la fin layout.addStretch() return group def apply_tips_mode(self, mode): # Arrêter TOUS les timers avant de changer de mode if hasattr(self, 'tip_animation_timer') and self.tip_animation_timer.isActive(): self.tip_animation_timer.stop() if hasattr(self, 'tip_change_timer') and self.tip_change_timer.isActive(): self.tip_change_timer.stop() if hasattr(self, 'tips_fade_timer') and self.tips_fade_timer.isActive(): self.tips_fade_timer.stop() if mode == 1: self.setup_scrolling_tips() self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Défilant") elif mode == 2: self.setup_fading_tips() self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Fondu") else: self.tips_label.clear() self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Désactivé") def toggle_tips_mode(self): """Change le mode d'affichage des conseils entre désactivé, défilant et aléatoire avec fondu""" try: # Passer au mode suivant (cycle entre 0, 1, 2) self.tips_mode = (self.tips_mode + 1) % 3 # Nettoyage général des timers avant tout if hasattr(self, 'tip_animation_timer') and self.tip_animation_timer.isActive(): self.tip_animation_timer.stop() if hasattr(self, 'tip_change_timer') and self.tip_change_timer.isActive(): self.tip_change_timer.stop() if hasattr(self, 'tips_fade_timer') and self.tips_fade_timer.isActive(): self.tips_fade_timer.stop() # Appliquer le comportement selon le mode if self.tips_mode == 0: self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Désactivé") self.tips_label.setVisible(False) self.log_action("📴 Mode Tips : Désactivé") elif self.tips_mode == 1: self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Défilant") self.tips_label.setVisible(True) self.setup_scrolling_tips() self.tips_label.setWindowOpacity(1.0) self.log_action("📜 Mode Tips : Défilement") else: # self.tips_mode == 2 self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Fondu") self.tips_label.setVisible(True) self.setup_fading_tips() self.log_action("🌫️ Mode Tips : Fondu") # Sauvegarder le mode dans les paramètres settings = QSettings("Snoown2K", "MA2C") settings.setValue("tips_mode", self.tips_mode) settings.sync() except Exception as e: self.log_action(f"❌ Erreur dans toggle_tips_mode : {e}") def restore_tips_mode(self): """Restaure le mode tips (0, 1, 2) depuis les paramètres enregistrés""" try: settings = QSettings("Snoown2K", "MA2C") saved_mode = int(settings.value("tips_mode", 0)) self.tips_mode = saved_mode if saved_mode in [0, 1, 2] else 0 # Répliquer l’effet visuel du mode au lancement if self.tips_mode == 0: self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Désactivé") self.tips_label.setVisible(False) elif self.tips_mode == 1: self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Défilant") self.tips_label.setVisible(True) self.setup_scrolling_tips() self.tips_label.setWindowOpacity(1.0) elif self.tips_mode == 2: self.tips_control_button.setText("Cliquez pour changer de mode: Mode Tips Fondu") self.tips_label.setVisible(True) self.setup_fading_tips() self.log_action(f"Mode Tips restauré : {['Désactivé', 'Défilement', 'Fondu'][self.tips_mode]}") except Exception as e: self.log_action(f"⚠️ Erreur restauration mode Tips : {e}") # Nouvelle méthode pour le mode conseils en fondu def setup_fading_tips(self): """Configure l'animation des conseils en mode fondu aléatoire SANS défilement""" import random from PyQt5.QtWidgets import QGraphicsOpacityEffect # ARRÊTER complètement tous les timers de défilement if hasattr(self, 'tip_animation_timer') and self.tip_animation_timer.isActive(): self.tip_animation_timer.stop() if hasattr(self, 'tip_change_timer') and self.tip_change_timer.isActive(): self.tip_change_timer.stop() # Liste des conseils tips = [ "Videz corbeille pour libérer l'espace", "Modifiez mots-clés dans les paramètres", "Clic sur lien ouvre le mini-navigateur", "Profils sauvent vos critères de scan", "Pensez à consulter le journal de log", "Sauvez profil avant de lancer le scan" ] # Sélectionner un conseil aléatoire self.current_tip = random.choice(tips) # Configurer le label pour un affichage STATIQUE centré self.tips_label.setAlignment(Qt.AlignCenter) self.tips_label.setText(self.current_tip) # Texte fixe, pas de défilement # Style initial avec opacité 0 self.tips_label.setStyleSheet(""" QLabel { background-color: #2c3e50; color: #42effb; padding: 5px; border-radius: 3px; font-size: 12px; font-weight: bold; } """) # Initialiser les variables pour l'effet de fondu self.tips_opacity = 0.0 self.fade_phase = 0 # 0: fade in, 1: hold, 2: fade out self.fade_counter = 0 self.hold_duration = 40 # Nombre de tics pour maintenir l'affichage (4 secondes) # Créer le timer pour l'effet de fondu uniquement self.tips_fade_timer = QTimer(self) self.tips_fade_timer.timeout.connect(self.update_fading_tip) self.tips_fade_timer.start(100) # Actualisation tous les 100ms # Méthode pour mettre à jour l'animation de fondu def update_fading_tip(self): """Met à jour l'animation du conseil en fondu SANS défilement""" from PyQt5.QtWidgets import QGraphicsOpacityEffect if self.fade_phase == 0: # Fade in self.tips_opacity += 0.05 # Plus lent pour un effet plus doux if self.tips_opacity >= 1.0: self.tips_opacity = 1.0 self.fade_phase = 1 # Passer à la phase de maintien self.fade_counter = 0 elif self.fade_phase == 1: # Hold (maintien) self.fade_counter += 1 if self.fade_counter >= self.hold_duration: self.fade_phase = 2 # Passer à la phase de fondu sortant elif self.fade_phase == 2: # Fade out self.tips_opacity -= 0.05 # Plus lent pour un effet plus doux if self.tips_opacity <= 0.0: self.tips_opacity = 0.0 self.fade_phase = 0 # Recommencer le cycle # Changer le conseil pour le prochain cycle import random tips = [ "Videz corbeille pour libérer l'espace", "Modifiez mots-clés dans les paramètres", "Clic sur lien ouvre le mini-navigateur", "Profils sauvent vos critères de scan", "Pensez à consulter le journal de log", "Sauvez profil avant de lancer le scan" ] # Sélectionner un nouveau conseil différent du précédent new_tip = self.current_tip while new_tip == self.current_tip and len(tips) > 1: new_tip = random.choice(tips) self.current_tip = new_tip # Mettre à jour le texte STATIQUE (pas de défilement) self.tips_label.setText(self.current_tip) # Appliquer l'effet d'opacité graphique effect = QGraphicsOpacityEffect(self.tips_label) effect.setOpacity(self.tips_opacity) self.tips_label.setGraphicsEffect(effect) def setup_scrolling_tips(self): """Configure l'animation du texte défilant avec des conseils aléatoires""" # Liste des conseils tips = [ "Videz corbeille pour libérer l'espace", "Modifiez mots-clés dans les paramètres", "Clic sur lien ouvre le mini-navigateur", "Profils sauvent vos critères de scan", "Pensez à consulter le journal de log", "Sauvez profil avant de lancer le scan" ] # Sélectionner un conseil aléatoire import random self.current_tip = random.choice(tips) self.tips_label.setText(self.current_tip + " " * 50) # Ajouter plus d'espace après le texte # Configurer l'animation self.tip_animation_pos = 0 self.tip_animation_timer = QTimer(self) self.tip_animation_timer.timeout.connect(self.update_scrolling_tip) self.tip_animation_timer.start(50) # Mise à jour plus fréquente pour un défilement plus fluide # Timer pour changer de conseil régulièrement self.tip_change_timer = QTimer(self) self.tip_change_timer.timeout.connect(self.change_tip) self.tip_change_timer.start(15000) # Changer toutes les 15 secondes def update_scrolling_tip(self): """Met à jour la position du texte défilant""" if not hasattr(self, 'current_tip'): return # Faire défiler le texte de droite à gauche text = self.current_tip + " " * 50 # Plus d'espace entre les répétitions self.tip_animation_pos = (self.tip_animation_pos + 1) % len(text) scrolled_text = text[self.tip_animation_pos:] + text[:self.tip_animation_pos] # Mettre à jour le texte affiché self.tips_label.setText(scrolled_text) def change_tip(self): """Change le conseil affiché pour un nouveau conseil aléatoire""" import random tips = [ "Videz corbeille pour libérer l'espace", "Modifiez mots-clés dans les paramètres", "Clic sur lien ouvre le mini-navigateur", "Profils sauvent vos critères de scan", "Pensez à consulter le journal de log", "Sauvez profil avant de lancer le scan" ] # Sélectionner un nouveau conseil différent du conseil actuel new_tip = self.current_tip while new_tip == self.current_tip: new_tip = random.choice(tips) self.current_tip = new_tip self.tip_animation_pos = 0 # Réinitialiser la position # Méthode pour basculer l'interface d'authentification def toggle_auth_interface(self, state): """Méthode pour basculer l'interface d'authentification""" try: if state == Qt.Checked: from main import AuthDialog # Import local pour éviter les imports circulaires from PyQt5.QtWidgets import QDesktopWidget # Ajout de l'import manquant auth_dialog = AuthDialog(self, checkbox=self.auth_checkbox) auth_dialog.setStyleSheet(""" QDialog { background-color: #1a2a3a; } QLabel { color: white; } QLineEdit { background-color: #03699c; padding: 5px; border-radius: 4px; } QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """) # Centrer la boîte de dialogue screen = QDesktopWidget().screenGeometry() dialog_geometry = auth_dialog.geometry() x = screen.center().x() - dialog_geometry.width() // 2 y = screen.center().y() - dialog_geometry.height() // 2 auth_dialog.move(x, y) if auth_dialog.exec_() == QDialog.Accepted: self.log_action("Authentification validée avec succès") # Sauvegarder les valeurs si nécessaire self.url_field = auth_dialog.url_field.text() self.login_field = auth_dialog.login_field.text() self.password_field = auth_dialog.password_field.text() # Sauvegarder les identifiants pour usage ultérieur self.outlook_handler.username = self.login_field self.outlook_handler.password = self.password_field # Proposer le remplissage automatique si on est sur une page if hasattr(self, 'mini_browser') and self.mini_browser: current_tab = self.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): current_url = current_tab.web_view.url().toString() if current_url and current_url != "about:blank": reply = QMessageBox.question( self, "Remplissage automatique", "Voulez-vous remplir automatiquement les champs de cette page ?", QMessageBox.Yes | QMessageBox.No ) if reply == QMessageBox.Yes: self.auto_fill_credentials() else: self.log_action("Authentification annulée") self.auth_checkbox.setChecked(False) except Exception as e: self.log_action(f"Erreur lors de l'authentification: {str(e)}") self.auth_checkbox.setChecked(False) def open_scheduler(self): """Ouvre le planificateur de tâches du volet paramètre""" # Vérifie si la MainWindow est accessible main_window = None parent = self.parent() # Remonter la hiérarchie des widgets pour trouver la MainWindow while parent: if hasattr(parent, 'show_scheduler'): main_window = parent break parent = parent.parent() # Si la MainWindow est trouvée, appeler sa méthode show_scheduler if main_window: main_window.show_scheduler() else: # Si MainWindow n'est pas accessible, ouvrir d'abord le volet des paramètres if hasattr(self, 'toggle_sidebar') and callable(self.toggle_sidebar): self.toggle_sidebar() # Attendre un peu pour que le sidebar s'ouvre QTimer.singleShot(300, lambda: self.parent().show_scheduler() if hasattr(self.parent(), 'show_scheduler') else None) def create_progress_section(self): group = QGroupBox("Progression et Statut") group.setMaximumHeight(400) # Hauteur réduite group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) layout = QVBoxLayout(group) # Styles différenciés pour chaque type de frame status_frame_style = """ QFrame { background-color: #1a2a3a; border-radius: 4px; padding: 4px; } QLabel { color: white; font-size: 12px; } """ count_frame_style = """ QFrame { background-color: #1a2a3a; border-radius: 4px; padding: 4px; } QLabel { color: white; font-size: 12px; } """ reward_frame_style = """ QFrame { background-color: #1a2a3a; border-radius: 4px; padding: 4px; } QLabel { color: white; font-size: 12px; } """ time_frame_style = """ QFrame { background-color: #1a2a3a; border-radius: 4px; padding: 4px; } QLabel { color: white; font-size: 12px; } """ counter_frame_style = """ QFrame { background-color: #1a2a3a; border-radius: 4px; padding: 4px; } QLabel { color: white; font-size: 12px; } """ # Barre de progression self.progress_bar = AnimatedProgressBar() self.progress_bar.setTextVisible(True) self.progress_bar.setFormat("%p%") self.progress_bar.setMinimumHeight(20) layout.addWidget(self.progress_bar) # Container pour status et comptage status_container = QHBoxLayout() status_container.setSpacing(5) # Status Frame status_frame = QFrame() status_frame.setStyleSheet(status_frame_style) status_layout = QVBoxLayout(status_frame) status_layout.setSpacing(1) status_layout.setContentsMargins(2, 2, 2, 2) status_title = QLabel("📊 Status") status_title.setAlignment(Qt.AlignCenter) self.status_label = QLabel("En attente de démarrage...") self.status_label.setStyleSheet("color: #5babfb;") self.status_label.setAlignment(Qt.AlignCenter) status_layout.addWidget(status_title) status_layout.addWidget(self.status_label) # Count Frame count_frame = QFrame() count_frame.setStyleSheet(count_frame_style) count_layout = QVBoxLayout(count_frame) count_layout.setSpacing(1) count_layout.setContentsMargins(2, 2, 2, 2) count_title = QLabel("🔢 Comptage") count_title.setAlignment(Qt.AlignCenter) self.count_label = QLabel("Nombre d'emails: 0") self.count_label.setStyleSheet("color: #5babfb;") self.count_label.setAlignment(Qt.AlignCenter) count_layout.addWidget(count_title) count_layout.addWidget(self.count_label) # Ajouter les frames au container status_container.addWidget(status_frame) status_container.addWidget(count_frame) layout.addLayout(status_container) # Afficheur de prochaine tâche planifiée self.next_task_label = QLabel() self.next_task_label.setStyleSheet(""" QLabel { background-color: #2c3e50; color: #65ddfb; padding: 8px; border-radius: 4px; margin-top: 5px; font-size: 11px; } """) self.update_next_task_display() layout.addWidget(self.next_task_label) # Container pour les indicateurs indicators_container = QHBoxLayout() indicators_container.setSpacing(5) # Reward Display Frame reward_frame = QFrame() reward_frame.setStyleSheet(reward_frame_style) reward_layout = QVBoxLayout(reward_frame) reward_layout.setSpacing(1) reward_layout.setContentsMargins(2, 2, 2, 2) reward_title = QLabel("💰 Récompense") reward_title.setAlignment(Qt.AlignCenter) self.reward_display = QLabel("---") self.reward_display.setStyleSheet("color: #5babfb;") self.reward_display.setAlignment(Qt.AlignCenter) reward_layout.addWidget(reward_title) reward_layout.addWidget(self.reward_display) # Time Display Frame time_frame = QFrame() time_frame.setStyleSheet(time_frame_style) time_layout = QVBoxLayout(time_frame) time_layout.setSpacing(1) time_layout.setContentsMargins(2, 2, 2, 2) time_title = QLabel("⏱️ Temps") time_title.setAlignment(Qt.AlignCenter) self.time_display = QLabel("--:--") self.time_display.setStyleSheet("color: #5babfb;") self.time_display.setAlignment(Qt.AlignCenter) time_layout.addWidget(time_title) time_layout.addWidget(self.time_display) # Email Counter Frame counter_frame = QFrame() counter_frame.setStyleSheet(counter_frame_style) counter_layout = QVBoxLayout(counter_frame) counter_layout.setSpacing(1) counter_layout.setContentsMargins(2, 2, 2, 2) counter_title = QLabel("📧 Emails") counter_title.setAlignment(Qt.AlignCenter) self.email_counter = QLabel("---") self.email_counter.setStyleSheet("color: #FFD700;") self.email_counter.setAlignment(Qt.AlignCenter) counter_layout.addWidget(counter_title) counter_layout.addWidget(self.email_counter) # Ajouter les frames au container indicators_container.addWidget(reward_frame) indicators_container.addWidget(time_frame) indicators_container.addWidget(counter_frame) layout.addLayout(indicators_container) # Créer un container dédié pour le timer de pause de cycle cycle_timer_container = QHBoxLayout() cycle_timer_container.setSpacing(0) # Créer le timer de pause entre cycles self.cycle_pause_timer = CyclePauseTimer() cycle_timer_container.addWidget(self.cycle_pause_timer) # Ajouter le container au layout principal layout.addLayout(cycle_timer_container) return group def update_display_time(self, min_value, max_value): """Met à jour le temps d'affichage dans la section info""" if hasattr(self, 'display_value'): self.display_value.setText(f"{min_value}-{max_value}s") # Mise à jour du handler si nécessaire if hasattr(self, 'outlook_handler'): self.outlook_handler.set_pause_time_range(min_value, max_value) def update_pause_time(self, min_value, max_value): """Met à jour le temps de pause dans la section info""" if hasattr(self, 'pause_value'): self.pause_value.setText(f"{min_value}-{max_value}s") # Mise à jour du handler si nécessaire if hasattr(self, 'outlook_handler'): self.outlook_handler.set_between_pages_time_range(min_value, max_value) def update_links_count(self, value): # S'assurer que la valeur est bien un entier value = int(value) # Mettre à jour la valeur interne self.links_count = value # Transmettre à l'OutlookHandler if hasattr(self, 'outlook_handler') and self.outlook_handler: self.outlook_handler.set_links_count(value) # Log pour confirmer self.log_action(f"Nombre de liens traités simultanément mis à jour: {value}") def init_email_counter(self, messages, max_emails): """Initialise le compteur avec le nombre total d'emails dans le dossier""" try: total_in_folder = messages.Count to_process = min(total_in_folder, max_emails) if max_emails else total_in_folder counter_text = f"0/{to_process}" if max_emails and max_emails < total_in_folder: counter_text += f" ({total_in_folder})" self.email_counter.setText(counter_text) QApplication.processEvents() except Exception as e: self.log_action(f"Erreur d'initialisation du compteur: {str(e)}") def update_email_counter(self, messages, processed_count=0, is_scanning=False): """ Met à jour le compteur avec le nombre réel d'emails dans le dossier Args: messages: Items du dossier Outlook processed_count: Nombre d'emails traités dans le scan actuel is_scanning: Si True, nous sommes en cours de scan """ try: current_total = messages.Count # Nombre actuel d'emails dans le dossier if is_scanning: # Pendant le scan, on soustrait uniquement les emails traités display_count = current_total - processed_count else: # Hors scan, on affiche simplement le total actuel display_count = current_total self.email_counter.setText(str(display_count)) QApplication.processEvents() except Exception as e: self.log_action(f"Erreur de mise à jour du compteur: {str(e)}") def setup_email_counter(self): """Configure le timer de mise à jour du compteur""" self.count_update_timer = QTimer(self) self.count_update_timer.timeout.connect(self.refresh_email_count) self.count_update_timer.start(60000) # Mise à jour toute les minutes def refresh_email_count(self): """Rafraîchit le compteur en dehors d'un scan""" try: if not hasattr(self, 'current_folder'): return messages = self.current_folder.Items self.update_email_counter(messages) except Exception as e: self.log_action(f"Erreur lors du rafraîchissement du compteur: {str(e)}") def update_next_task_display(self): """Met à jour l'affichage de la prochaine tâche avec protection complète""" try: # === PROTECTION WIDGETS === # Vérifier que les widgets essentiels existent avant de continuer required_widgets = ['next_task_label'] for widget_name in required_widgets: if not hasattr(self, widget_name): # Widgets pas encore créés - sortie silencieuse return # === INITIALISATION === next_task_text = "Aucune tâche planifiée" task_count = 0 next_task_time = None # === RÉCUPÉRATION DES TÂCHES OUTLOOK === if self.outlook_handler: try: # Tentative de récupération des tâches via Outlook tasks = self.outlook_handler.get_upcoming_tasks() if hasattr(self.outlook_handler, 'get_upcoming_tasks') else [] if tasks: task_count = len(tasks) # Prendre la première tâche (la plus proche) next_task = tasks[0] # Formatage de l'affichage de la tâche if hasattr(next_task, 'Subject') and hasattr(next_task, 'DueDate'): subject = next_task.Subject[:50] + "..." if len( next_task.Subject) > 50 else next_task.Subject due_date = next_task.DueDate # Formatage de la date if due_date: try: from datetime import datetime if hasattr(due_date, 'strftime'): date_str = due_date.strftime("%d/%m/%Y %H:%M") else: date_str = str(due_date) next_task_text = f"📋 {subject}\n⏰ {date_str}" next_task_time = due_date except: next_task_text = f"📋 {subject}\n⏰ Date non disponible" else: next_task_text = f"📋 {subject}\n⏰ Pas de date d'échéance" else: next_task_text = f"📋 Tâche disponible\n📊 {task_count} tâche(s) au total" else: next_task_text = "✅ Aucune tâche planifiée" except Exception as task_error: next_task_text = f"⚠️ Erreur accès tâches Outlook\n{str(task_error)[:30]}..." self.log_action(f"❌ Erreur récupération tâches Outlook: {task_error}") # === RÉCUPÉRATION ALTERNATIVE (TÂCHES SYSTÈME) === elif hasattr(self, 'get_system_tasks'): try: system_tasks = self.get_system_tasks() if system_tasks: task_count = len(system_tasks) next_task_text = f"🔧 {task_count} tâche(s) système\n⏳ Prochaine: {system_tasks[0]['name'][:30]}" else: next_task_text = "✅ Aucune tâche système" except Exception as sys_error: next_task_text = f"⚠️ Erreur tâches système\n{str(sys_error)[:30]}..." self.log_action(f"❌ Erreur tâches système: {sys_error}") # === MISE À JOUR DE L'AFFICHAGE === # Widget principal des tâches self.next_task_label.setText(next_task_text) # Mise à jour du compteur de tâches (si le widget existe) if hasattr(self, 'task_count_label'): count_text = f"{task_count} tâche(s)" if task_count > 0 else "Aucune tâche" self.task_count_label.setText(count_text) # Mise à jour de l'indicateur de priorité (si le widget existe) if hasattr(self, 'task_priority_indicator'): if task_count > 0 and next_task_time: try: from datetime import datetime, timedelta now = datetime.now() if hasattr(next_task_time, 'replace'): # Convertir en datetime si nécessaire if hasattr(next_task_time, 'date'): time_diff = next_task_time - now else: time_diff = timedelta(days=1) # Valeur par défaut # Définir la couleur selon l'urgence if time_diff.total_seconds() < 3600: # Moins d'1 heure self.task_priority_indicator.setStyleSheet("background-color: #ff4444; color: white;") self.task_priority_indicator.setText("🚨 URGENT") elif time_diff.total_seconds() < 86400: # Moins d'1 jour self.task_priority_indicator.setStyleSheet("background-color: #ff8800; color: white;") self.task_priority_indicator.setText("⚠️ PROCHE") else: self.task_priority_indicator.setStyleSheet("background-color: #00aa00; color: white;") self.task_priority_indicator.setText("✅ PLANIFIÉ") else: self.task_priority_indicator.setStyleSheet("background-color: #888888; color: white;") self.task_priority_indicator.setText("❓ INCONNU") except: self.task_priority_indicator.setStyleSheet("background-color: #888888; color: white;") self.task_priority_indicator.setText("❓ ERREUR") else: self.task_priority_indicator.setStyleSheet("background-color: #cccccc; color: #666666;") self.task_priority_indicator.setText("⭕ AUCUNE") # === LOG DE SUCCÈS === if task_count > 0: self.log_action(f"📋 Tâches mises à jour: {task_count} trouvée(s)") except Exception as e: # === GESTION D'ERREUR ROBUSTE === error_message = f"❌ Erreur update_next_task_display: {str(e)[:50]}..." # Tentative de mise à jour du widget principal if hasattr(self, 'next_task_label'): try: self.next_task_label.setText("⚠️ Erreur récupération tâches\nVérifiez la connexion Outlook") except: pass # Si même cette opération échoue, on continue silencieusement # Mise à jour des widgets secondaires avec des valeurs d'erreur if hasattr(self, 'task_count_label'): try: self.task_count_label.setText("Erreur") except: pass if hasattr(self, 'task_priority_indicator'): try: self.task_priority_indicator.setStyleSheet("background-color: #cc0000; color: white;") self.task_priority_indicator.setText("❌ ERREUR") except: pass # Log de l'erreur if hasattr(self, 'log_action'): try: self.log_action(error_message) except: print(error_message) # Fallback vers print si log_action échoue else: print(error_message) # Log détaillé pour debug (optionnel) if hasattr(self, 'log_action'): try: import traceback detailed_error = traceback.format_exc() self.log_action(f"🔍 Détail erreur tâches: {detailed_error[:200]}...") except: pass # Si le log détaillé échoue, on continue def get_upcoming_tasks(self): """Méthode auxiliaire pour récupérer les tâches (à adapter selon votre logique Outlook)""" try: if not self.outlook_handler: return [] # Exemple d'implémentation - adaptez selon votre handler Outlook if hasattr(self.outlook_handler, 'get_tasks'): return self.outlook_handler.get_tasks(limit=10) elif hasattr(self.outlook_handler, 'outlook') and hasattr(self.outlook_handler.outlook, 'GetNamespace'): namespace = self.outlook_handler.outlook.GetNamespace("MAPI") tasks_folder = namespace.GetDefaultFolder(13) # 13 = olFolderTasks tasks = tasks_folder.Items tasks.Sort("[DueDate]") # Trier par date d'échéance # Retourner les 10 premières tâches non terminées upcoming_tasks = [] for task in tasks: if len(upcoming_tasks) >= 10: break if hasattr(task, 'Complete') and not task.Complete: upcoming_tasks.append(task) return upcoming_tasks else: return [] except Exception as e: if hasattr(self, 'log_action'): self.log_action(f"❌ Erreur get_upcoming_tasks: {e}") return [] def get_system_tasks(self): """Méthode auxiliaire pour les tâches système (placeholder)""" try: # Exemple de tâches système - adaptez selon vos besoins system_tasks = [ {"name": "Nettoyage cache", "time": "14:00", "priority": "low"}, {"name": "Sauvegarde données", "time": "18:00", "priority": "high"}, {"name": "Mise à jour système", "time": "02:00", "priority": "medium"} ] return system_tasks except: return [] # Timer pour mettre à jour l'affichage régulièrement def setup_task_update_timer(self): """Configure le timer pour mettre à jour l'affichage de la prochaine tâche""" self.task_update_timer = QTimer() self.task_update_timer.timeout.connect(self.update_next_task_display) self.task_update_timer.start(300000) # Mise à jour toutes les 5 minutes def start_scan_timer(self, duration_seconds): self.remaining_time = duration_seconds self.update_time_display() self.scan_timer.start(1000) # Met à jour toutes les secondes def stop_scan_timer(self): self.scan_timer.stop() self.time_display.setText("--:--") def update_time_display(self): # Ne mettez pas à jour si l'afficheur est en pause if hasattr(self, 'time_display_paused') and self.time_display_paused: return if self.remaining_time > 0: minutes = self.remaining_time // 60 seconds = self.remaining_time % 60 self.time_display.setText(f"{minutes:02d}:{seconds:02d}") self.remaining_time -= 1 else: self.stop_scan_timer() def calculate_scan_duration(self, max_emails, pause_time): # Estimation du temps total basée sur le nombre d'emails et le temps de pause estimated_duration = max_emails * (pause_time + 2) # +2 secondes pour le traitement return estimated_duration def create_browser_section(self): group = QGroupBox("Navigateur et Journal") layout = QHBoxLayout(group) layout.setSpacing(5) layout.setContentsMargins(0, 0, 0, 0) # --- Container principal pour les contrôles --- controls_container = QWidget() controls_layout = QHBoxLayout(controls_container) controls_layout.setSpacing(20) # Espace entre les deux groupes controls_layout.setContentsMargins(5, 0, 5, 5) # --- GROUPE ZOOM --- zoom_container = QWidget() zoom_layout = QVBoxLayout(zoom_container) zoom_layout.setSpacing(4) zoom_layout.setContentsMargins(0, 0, 0, 0) zoom_layout.setAlignment(Qt.AlignCenter) # Centre tous les éléments # Label Zoom zoom_label = QLabel("Zoom affichage") zoom_label.setStyleSheet(""" QLabel { color: #FFFFFF; font-size: 11px; margin-bottom: 2px; font-weight: bold; } """) zoom_label.setAlignment(Qt.AlignCenter) zoom_layout.addWidget(zoom_label) # Container pour les boutons + et - zoom_container = QWidget() zoom_layout = QVBoxLayout(zoom_container) zoom_buttons_layout.setSpacing(4) # Réduit l'espace entre les boutons zoom_buttons_layout.setContentsMargins(0, 0, 0, 0) zoom_buttons_layout.setAlignment(Qt.AlignCenter) # Centre les boutons self.zoom_out_button = QPushButton("-") self.zoom_in_button = QPushButton("+") buttons_style = """ QPushButton { background-color: #008CBA; color: white; border: none; border-radius: 3px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """ # Configuration et ajout des boutons for button in [self.zoom_out_button, self.zoom_in_button]: button.setFixedSize(24, 24) button.setStyleSheet(buttons_style) zoom_buttons_layout.addWidget(button) # --- GROUPE MODE D'AFFICHAGE --- display_container = QWidget() display_layout = QVBoxLayout(display_container) display_layout.setSpacing(4) display_layout.setContentsMargins(0, 0, 0, 0) display_layout.setAlignment(Qt.AlignCenter) # Label Mode d'affichage display_label = QLabel("Mode d'affichage") display_label.setStyleSheet(""" QLabel { color: #FFFFFF; font-size: 11px; margin-bottom: 2px; font-weight: bold; } """) display_label.setAlignment(Qt.AlignCenter) display_layout.addWidget(display_label) self.toggle_browser_button = QPushButton("AGRANDIR") self.toggle_browser_button.setFixedSize(85, 24) self.toggle_browser_button.setStyleSheet(""" QPushButton { background-color: #008CBA; color: white; border: none; border-radius: 3px; font-weight: bold; font-size: 11px; } QPushButton:hover { background-color: #007B9A; } """) display_layout.addWidget(self.toggle_browser_button) controls_layout.addWidget(zoom_container) controls_layout.addWidget(display_container) # Ajouter le container au layout du navigateur browser_layout.addWidget(controls_container) # Zone de texte pour le log self.log_text = QTextEdit() self.log_text.setReadOnly(True) self.log_text.setStyleSheet(""" QTextEdit { background-color: #1e1e1e; color: #ffffff; border: 1px solid #34495e; border-radius: 4px; padding: 10px; font-family: Consolas, monospace; } QScrollBar:vertical { background: #2d2d2d; width: 12px; margin: 0px; } QScrollBar::handle:vertical { background-color: #4a4a4a; min-height: 20px; border-radius: 6px; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } """) log_layout.addWidget(self.log_text) # Ajouter le conteneur de log au layout principal layout.addWidget(log_container) return group def adjust_mini_browser_content(self): try: if self.mini_browser: # Injecter du JavaScript pour ajuster le contenu js_code = """ (function() { // Permettre seulement le défilement horizontal document.documentElement.style.overflowY = 'hidden'; document.documentElement.style.overflowX = 'auto'; document.body.style.overflowY = 'hidden'; document.body.style.overflowX = 'auto'; document.body.style.margin = '0'; document.body.style.padding = '0'; // SUPPRESSION DU SCALING AUTOMATIQUE - on garde seulement le contrôle du scroll // Le zoom est maintenant contrôlé uniquement par setZoomFactor() // Forcer le rafraîchissement window.dispatchEvent(new Event('resize')); })(); """ current_tab = self.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): current_tab.web_view.page().runJavaScript(js_code) except Exception as e: self.log_action(f"Erreur lors de l'ajustement du mini navigateur: {str(e)}") def initialize_browser_zoom(self): """Initialise le zoom du navigateur en fonction des paramètres ou du profil actuel""" try: # Obtenir le zoom par défaut des paramètres settings = QSettings("VotreEntreprise", "AutoLook") default_zoom = settings.value("mini_browser_zoom", 0.3, type=float) # Vérifier si un profil est actif et récupérer son zoom zoom_factor = default_zoom if hasattr(self.main_window, 'current_profile') and self.main_window.current_profile: current_profile = self.main_window.current_profile.replace("⭐ ", "") profile_path = os.path.join(r"C:\autolook\profiles", f"{current_profile}.json") if os.path.exists(profile_path): try: with open(profile_path, 'r', encoding='utf-8') as f: profile_data = json.load(f) if 'settings' in profile_data and 'browser_zoom' in profile_data['settings']: zoom_factor = profile_data['settings']['browser_zoom'] logger.info(f"Zoom initialisé depuis le profil: {zoom_factor * 100:.0f}%") except Exception as e: logger.error(f"Erreur lors de l'initialisation du zoom: {str(e)}") # CORRECTION : Appliquer le zoom au mini navigateur via l'onglet actuel if self.mini_browser: current_tab = self.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): current_tab.web_view.setZoomFactor(zoom_factor) # Mettre à jour les tooltips des boutons de zoom if hasattr(self, 'zoom_in_button') and hasattr(self, 'zoom_out_button'): zoom_percentage = int(zoom_factor * 100) tooltip_text = f'Zoom actuel: {zoom_percentage}%' self.zoom_in_button.setToolTip(tooltip_text) self.zoom_out_button.setToolTip(tooltip_text) if hasattr(self, 'zoom_reset_button'): self.zoom_reset_button.setToolTip(tooltip_text) self.log_action(f"🔧 Zoom initialisé à {zoom_factor * 100:.0f}%") return zoom_factor except Exception as e: logger.error(f"Erreur dans initialize_browser_zoom: {str(e)}") self.log_action(f"❌ Erreur initialisation zoom: {str(e)}") return 0.3 # Valeur par défaut en cas d'erreur def save_zoom_level(self, zoom_level): """Sauvegarde le niveau de zoom dans un fichier texte simple""" try: import os # S'assurer que le répertoire existe zoom_dir = r"C:\AUTOLOOK" os.makedirs(zoom_dir, exist_ok=True) # Chemin du fichier de configuration - format texte simple zoom_file = os.path.join(zoom_dir, "miniview.json") # Sauvegarder le zoom en tant que texte simple with open(zoom_file, 'w') as f: f.write(str(zoom_level)) # Message de débogage logger.info(f"Zoom sauvegardé: {zoom_level * 100:.0f}% dans {zoom_file}") except Exception as e: logger.error(f"Erreur lors de la sauvegarde du niveau de zoom: {str(e)}") def expand_all_folders(self): """Déplie tous les dossiers""" for i in range(self.folder_listbox.count()): item = self.folder_listbox.item(i) data = item.data(Qt.UserRole) if data['has_subfolders'] and not data['is_expanded']: item.setHidden(False) self.toggle_folder(item) def collapse_all_folders(self): """Replie tous les dossiers""" for i in range(self.folder_listbox.count()): item = self.folder_listbox.item(i) data = item.data(Qt.UserRole) if data['has_subfolders'] and data['is_expanded']: self.toggle_folder(item) def refresh_folders(self): """Rafraîchit la liste des dossiers""" if hasattr(self, 'mailbox_listbox') and self.mailbox_listbox.currentItem(): selected_mailbox = self.mailbox_listbox.currentItem().text() self.update_folders(selected_mailbox) self.log_action("Liste des dossiers actualisée") def show_folder_stats(self): """Affiche les statistiques du dossier sélectionné""" self.setCursor(Qt.WaitCursor) # Curseur de chargement try: if not self.folder_listbox.currentItem(): QMessageBox.warning(self, "Erreur", "Veuillez sélectionner un dossier") return self.log_action("Récupération des statistiques en cours...") # Récupération correcte de l'ID du dossier depuis l'item folder_data = self.folder_listbox.currentItem().data(Qt.UserRole) if not folder_data or 'id' not in folder_data: self.log_action("Erreur: Pas d'ID de dossier trouvé") QMessageBox.warning(self, "Erreur", "Impossible de récupérer les informations du dossier") return folder_id = folder_data['id'] self.log_action(f"ID du dossier: {folder_id}") if not hasattr(self, 'folder_stats'): self.folder_stats = FolderStats(self.outlook_handler) stats = self.folder_stats.get_folder_stats(folder_id) if stats: # Création de la boîte de dialogue dialog = QDialog(self) dialog.setWindowTitle(f"Statistiques - {stats['folder_name']}") dialog.setMinimumWidth(600) dialog.setMinimumHeight(400) layout = QVBoxLayout() # Création du texte des statistiques avec mise en forme HTML stats_text = QTextEdit() stats_text.setReadOnly(True) # Formatage des statistiques avec du HTML pour un meilleur rendu html_content = f"""
📊 Informations générales
• Nombre total de messages : {stats['total_messages']}
• Messages non lus : {stats['unread_messages']}
• Taux de lecture : {(100 - (stats['unread_messages'] / stats['total_messages'] * 100 if stats['total_messages'] > 0 else 0)):.1f}%
⏱️ Distribution temporelle
• Premier message : {stats['time_stats']['oldest_message']}
• Dernier message : {stats['time_stats']['newest_message']}
💾 Analyse de la taille
• Taille totale : {self._format_size(stats['size_stats']['total_size'])}
• Taille moyenne : {self._format_size(stats['size_stats']['average_size'])}
• Plus grand message : {self._format_size(stats['size_stats']['max_size'])}
👥 Top 5 des expéditeurs
""" for sender, count in list(stats['sender_stats'].items())[:5]: html_content += f""" """ html_content += """
Expéditeur Nombre de messages
{sender} {count}
""" stats_text.setHtml(html_content) layout.addWidget(stats_text) # Boutons de contrôle button_layout = QHBoxLayout() refresh_button = QPushButton("Rafraîchir") refresh_button.clicked.connect(lambda: self.refresh_stats(folder_id, stats_text)) close_button = QPushButton("Fermer") close_button.clicked.connect(dialog.accept) for button in [refresh_button, close_button]: button.setStyleSheet(""" QPushButton { background-color: #008CBA; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #007B9A; } """) button_layout.addWidget(button) layout.addLayout(button_layout) dialog.setLayout(layout) dialog.exec_() else: self.log_action("Erreur: Impossible de récupérer les statistiques") QMessageBox.warning(self, "Erreur", "Impossible de récupérer les statistiques du dossier") except Exception as e: self.log_action(f"Erreur lors de l'affichage des statistiques : {str(e)}") QMessageBox.critical(self, "Erreur", f"Une erreur s'est produite : {str(e)}") finally: self.setCursor(Qt.ArrowCursor) def refresh_stats(self, folder_id, stats_text): """Rafraîchit l'affichage des statistiques""" try: stats = self.folder_stats.get_folder_stats(folder_id) if stats: self.update_stats_display(stats, stats_text) except Exception as e: QMessageBox.warning(self, "Erreur", f"Erreur lors du rafraîchissement des statistiques : {str(e)}") def _format_size(self, size_in_bytes): """Formate une taille en bytes en format lisible""" for unit in ['B', 'KB', 'MB', 'GB']: if size_in_bytes < 1024: return f"{size_in_bytes:.1f} {unit}" size_in_bytes /= 1024 return f"{size_in_bytes:.1f} TB" def sync_url(self): """Synchronise l'URL entre le mini navigateur et le navigateur normal""" current_url = None try: # Obtenir l'URL actuellement affichée if self.is_mini_browser and self.mini_browser: current_url = self.mini_browser.web_view.url().toString() elif not self.is_mini_browser and self.browser_window: current_url = self.browser_window.browser.url().toString() # Si nous avons une URL valide, la synchroniser entre les navigateurs if current_url: if self.is_mini_browser and self.browser_window: self.browser_window.browser.load(QUrl(current_url)) elif not self.is_mini_browser and self.mini_browser: self.mini_browser.load_url(url, display_time) self.log_action(f"URL synchronisée: {current_url}") except Exception as e: self.log_action(f"Erreur de synchronisation URL: {str(e)}") def toggle_browser_view(self): """Bascule entre le mini navigateur et le navigateur normal""" try: self.is_mini_browser = not self.is_mini_browser if self.is_mini_browser: # Masquer le navigateur normal et afficher le mini navigateur if self.browser_window: self.browser_window.hide() if self.mini_browser: current_tab = self.browser_window.get_current_tab() current_url = current_tab.web_view.url().toString() if current_tab else None if current_url: mini_tab = self.mini_browser.get_current_tab() if mini_tab: mini_tab.web_view.load(QUrl(current_url)) self.mini_browser.setVisible(True) self.toggle_browser_button.setText("AGRANDIR") else: # Masquer le mini navigateur et afficher le navigateur normal if self.mini_browser: self.mini_browser.setVisible(False) mini_tab = self.mini_browser.get_current_tab() current_url = mini_tab.web_view.url().toString() if mini_tab else None if self.browser_window: if current_url: current_tab = self.browser_window.get_current_tab() if current_tab: current_tab.web_view.load(QUrl(current_url)) self.browser_window.show() self.toggle_browser_button.setText("RÉDUIRE") # Mise à jour du style du bouton self.toggle_browser_button.setStyleSheet(""" QPushButton { background-color: #008CBA; color: white; border: none; border-radius: 4px; font-weight: bold; font-size: 12px; min-width: 80px; padding: 5px; } QPushButton:hover { background-color: #007B9A; } """) self.log_action(f"Vue du navigateur basculée vers {'mini' if self.is_mini_browser else 'normal'}") except Exception as e: self.log_action(f"Erreur lors du basculement de vue: {str(e)}") def update_browser(self, url): """ VERSION MODIFIÉE : Charge réellement l'URL pour permettre l'interaction """ try: # === PROTECTION CONTRE RECHARGEMENT PENDANT LA PAUSE === if hasattr(self, '_threads_suspended') and self._threads_suspended: self.log_action(f"⏸️ Rechargement bloqué pendant la pause: {url[:50]}...") return if not isinstance(url, str): return display_time = self.outlook_handler.get_random_pause_time() # ═══ CHARGER LA VRAIE URL ═══ if self.mini_browser: current_tab = self.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): # CHARGER L'URL RÉELLE au lieu d'injecter du contenu current_tab.web_view.load(QUrl(url)) # Mettre à jour la console avec les infos if hasattr(self.mini_browser, 'console_header'): self.mini_browser.console_header.show() self.mini_browser.console_header.update_page_time(f"Display {display_time:.1f}s") # Enregistrer dans l'historique if isinstance(self.mini_browser, BrowserWindow): self.mini_browser.update_url(QUrl(url)) # === PROTECTION : Ne pas attendre si en pause === if not (hasattr(self, '_threads_suspended') and self._threads_suspended): # Simulation du temps d'affichage seulement si pas en pause from PyQt5.QtCore import QEventLoop, QTimer loop = QEventLoop() timer = QTimer() timer.setSingleShot(True) timer.timeout.connect(loop.quit) timer.start(int(display_time * 1000)) # Vérifier régulièrement si on passe en pause check_timer = QTimer() def check_pause(): if hasattr(self, '_threads_suspended') and self._threads_suspended: timer.stop() check_timer.stop() loop.quit() check_timer.timeout.connect(check_pause) check_timer.start(100) # Vérifier toutes les 100ms loop.exec_() check_timer.stop() self.outlook_handler.update_links_clicked() self.log_action(f"🌐 URL chargée: {url[:50]}... (temps: {display_time:.1f}s)") except Exception as e: self.log_action(f"Erreur chargement URL: {str(e)}") def auto_fill_credentials(self, username=None, password=None): """ Remplit automatiquement les champs de connexion si disponibles """ try: if not username and hasattr(self, 'login_field'): username = self.login_field if not password and hasattr(self, 'password_field'): password = self.password_field if not username or not password: self.log_action("⚠️ Identifiants non configurés - utilisez la checkbox Authentification") return if hasattr(self, 'mini_browser') and self.mini_browser: current_tab = self.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): # JavaScript pour remplir automatiquement les champs fill_js = f""" (function() {{ // Chercher les champs de connexion les plus communs var usernameSelectors = [ 'input[type="email"]', 'input[name*="email"]', 'input[name*="username"]', 'input[name*="login"]', 'input[id*="email"]', 'input[id*="username"]', 'input[id*="login"]' ]; var passwordSelectors = [ 'input[type="password"]', 'input[name*="password"]', 'input[name*="passwd"]', 'input[id*="password"]', 'input[id*="passwd"]' ]; // Remplir le nom d'utilisateur for (var selector of usernameSelectors) {{ var field = document.querySelector(selector); if (field) {{ field.value = '{username}'; field.dispatchEvent(new Event('input', {{ bubbles: true }})); console.log('Username rempli:', selector); break; }} }} // Remplir le mot de passe for (var selector of passwordSelectors) {{ var field = document.querySelector(selector); if (field) {{ field.value = '{password}'; field.dispatchEvent(new Event('input', {{ bubbles: true }})); console.log('Password rempli:', selector); break; }} }} // Essayer de trouver et cliquer le bouton de connexion var loginButtons = [ 'button[type="submit"]', 'input[type="submit"]', 'button:contains("Login")', 'button:contains("Sign in")', 'button:contains("Connexion")', '[class*="login"]', '[class*="signin"]' ]; return 'Champs remplis automatiquement'; }})(); """ current_tab.web_view.page().runJavaScript(fill_js, self._on_credentials_filled) except Exception as e: self.log_action(f"❌ Erreur remplissage automatique : {e}") def _on_credentials_filled(self, result): """Callback après remplissage des identifiants""" if result: self.log_action(f"✅ {result}") else: self.log_action("⚠️ Aucun champ de connexion détecté sur cette page") def cleanup_browser_resources(self): """Nettoie les ressources du navigateur entre les traitements""" try: if self.mini_browser: current_tab = self.mini_browser.get_current_tab() if current_tab and hasattr(current_tab, 'web_view'): # Nettoyer la page actuelle current_tab.web_view.page().setUrl(QUrl("about:blank")) # Forcer la collecte des déchets current_tab.web_view.page().runJavaScript("window.gc && window.gc();") # Forcer le nettoyage Python import gc gc.collect() except Exception as e: self.log_action(f"Erreur nettoyage navigateur: {str(e)}") def log_action(self, message, category='system', auto_scroll=True): # NOUVEAU: Vérification de suspension système if hasattr(self, '_threads_suspended') and self._threads_suspended: return # Ne pas logger pendant la pause système # Protection contre les appels récursifs if hasattr(self, '_logging_in_progress') and self._logging_in_progress: return self._logging_in_progress = True # Protection contre les appels récursifs if hasattr(self, '_logging_in_progress') and self._logging_in_progress: return self._logging_in_progress = True """ Méthode de logging avancée avec : - Auto-détection des catégories - Protection contre le spam - Gestion des couleurs et icônes - Contrôle d'activation par catégorie """ try: from PyQt5.QtCore import QDateTime from PyQt5.QtWidgets import QApplication import time # === 1. INITIALISATION DES SYSTÈMES === # Initialiser les catégories si pas encore fait if not hasattr(self, 'log_categories'): self.log_categories = { 'email': {'color': '#98FB98', 'icon': '📧', 'enabled': True}, 'count': {'color': '#FFB347', 'icon': '🔢', 'enabled': True}, 'error': {'color': '#FF6B6B', 'icon': '❌', 'enabled': True}, 'success': {'color': '#4CAF50', 'icon': '✅', 'enabled': True}, 'info': {'color': '#87CEEB', 'icon': 'ℹ️', 'enabled': True}, 'system': {'color': '#87CEEB', 'icon': '⚙️', 'enabled': True}, 'warning': {'color': '#FFA500', 'icon': '⚠️', 'enabled': True}, 'debug': {'color': '#DDA0DD', 'icon': '🔧', 'enabled': True} } # Initialiser le système anti-spam si pas encore fait if not hasattr(self, '_log_throttler'): self._log_throttler = { 'last_log_time': {}, # Dernier timestamp par type de message 'log_counts': {}, # Compteur par type de message 'throttle_interval': 0.5, # 0.5 seconde entre messages similaires 'max_identical_logs': 5, # Maximum 5 messages identiques consécutifs 'last_debug_time': 0, # Pour afficher les stats périodiquement 'last_process_events': 0 # Pour limiter processEvents() } # Initialiser le nom du journal si pas encore fait if not hasattr(self, 'current_log_name'): self.current_log_name = f"Journal - {QDateTime.currentDateTime().toString('dd/MM HH:mm')}" # === 2. AUTO-DÉTECTION DE CATÉGORIE === if category == 'system': message_lower = message.lower() # Détection d'erreur if any(word in message_lower for word in ['erreur', 'error', 'échec', 'failed', 'exception']): category = 'error' # Détection de succès elif any(word in message_lower for word in ['succès', 'success', 'terminé', 'completed', 'réussi']): category = 'success' # Détection d'email elif any(word in message_lower for word in ['email', 'mail', 'message', 'envoi', 'réception']): category = 'email' # Détection de comptage elif any(word in message_lower for word in ['comptage', 'count', 'nombre', 'total', 'trouvé']): category = 'count' # Détection de warning elif any(word in message_lower for word in ['attention', 'warning', 'avertissement']): category = 'warning' # Détection de debug elif any(word in message_lower for word in ['debug', 'débogage', 'test', 'vérification']): category = 'debug' # Détection d'info elif any(word in message_lower for word in ['info', 'information', 'démarrage', 'initialisation']): category = 'info' # === 3. VÉRIFICATION SI CATÉGORIE ACTIVÉE === if not self.log_categories.get(category, {}).get('enabled', True): return # Cette catégorie est désactivée # === 4. SYSTÈME ANTI-SPAM === current_time = time.time() throttler = self._log_throttler # Créer une clé unique pour ce type de message log_key = f"{category}:{hash(str(message))}" # Vérifier si on doit limiter ce message should_log = True if log_key in throttler['last_log_time']: time_diff = current_time - throttler['last_log_time'][log_key] # Si le message est trop récent, le compter mais ne pas l'afficher if time_diff < throttler['throttle_interval']: throttler['log_counts'][log_key] = throttler['log_counts'].get(log_key, 0) + 1 should_log = False else: # Réinitialiser le compteur après l'intervalle throttler['log_counts'][log_key] = 1 # Vérifier le nombre de messages identiques count = throttler['log_counts'].get(log_key, 0) if count > throttler['max_identical_logs']: should_log = False # Afficher les statistiques de suppression périodiquement if current_time - throttler['last_debug_time'] > 10.0: total_suppressed = sum(max(0, c - 1) for c in throttler['log_counts'].values()) if total_suppressed > 0: print(f"==> OutlookHandler: {total_suppressed} messages supprimés (anti-spam)") throttler['last_debug_time'] = current_time # Si le message est supprimé par l'anti-spam, on sort if not should_log: return # === 5. PRÉPARATION DU MESSAGE === # Mettre à jour les timestamps et compteurs throttler['last_log_time'][log_key] = current_time throttler['log_counts'][log_key] = throttler['log_counts'].get(log_key, 0) + 1 # Récupérer les infos de la catégorie cat_info = self.log_categories.get(category, self.log_categories['system']) icon = cat_info['icon'] color = cat_info['color'] # Créer le timestamp timestamp = QDateTime.currentDateTime().toString("HH:mm:ss") # Ajouter un compteur si le message a été répété count = throttler['log_counts'][log_key] message_with_count = message if count > 1: message_with_count += f" [x{count}]" # === 6. FORMATAGE ET AFFICHAGE === # Créer l'entrée de log avec HTML log_entry = f'
{timestamp} {icon} {message_with_count}
' # Vérifier que le widget de log existe if hasattr(self, 'log_text') and self.log_text: # Vérifier si le journal n'est pas désactivé if not (hasattr(self, 'toggle_log_button') and self.toggle_log_button.isChecked()): self.log_text.append(log_entry) # Auto-scroll si demandé if auto_scroll: scrollbar = self.log_text.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) # === 7. TRAITEMENT DES ÉVÉNEMENTS (AVEC LIMITATION) === self._logging_in_progress = False # Limiter processEvents() pour éviter les ralentissements if current_time - throttler['last_process_events'] > 0.1: # Max 10 fois par seconde QApplication.processEvents() throttler['last_process_events'] = current_time except Exception as e: # Éviter les boucles infinies dans la gestion d'erreur current_time = time.time() if not hasattr(self, '_last_error_log') or (current_time - self._last_error_log) > 5.0: print(f"Erreur dans log_action: {e}") self._last_error_log = current_time def sync_filters_to_main_log(self): """Synchronise les filtres vers le journal principal""" try: # Reconstruire le journal principal avec les filtres actuels original_content = self.log_text.toHtml() self.log_text.clear() for line in original_content.split('\n'): if '
80 else ''}", auto_scroll=True) if keywords_found: self.log_action(f"🔍 Mots-clés trouvés: {', '.join(keywords_found)}", auto_scroll=True) if link and link != 'Aucun lien': self.log_action(f"🔗 Lien: {link}", auto_scroll=True) if action == 'cliqué': self.log_action(f"✅ Action: Lien cliqué avec succès", auto_scroll=True) elif action == 'supprimé': self.log_action(f"🗑️ Action: Email supprimé", auto_scroll=True) elif action == 'ignoré': self.log_action(f"⏩ Action: Email ignoré", auto_scroll=True) else: self.log_action(f"⚙️ Action: {action}", auto_scroll=True) self.log_action(f"📧 ──────────────────────", auto_scroll=True) except Exception as e: self.log_action(f"❌ Erreur lors du log détaillé: {str(e)}", auto_scroll=True) def log_scan_progress(self, current, total, action="Progression"): """ Log de progression sans défilement automatique Args: current (int): Numéro actuel total (int): Total à traiter action (str): Action en cours """ percentage = int((current / total) * 100) if total > 0 else 0 progress_bar = "█" * (percentage // 5) + "░" * (20 - (percentage // 5)) message = f"📊 {action}: {current}/{total} ({percentage}%) [{progress_bar}]" self.log_action(message, auto_scroll=False) # Pas de défilement pour la progression def log_batch_summary(self, batch_info): """ Log de résumé d'un lot traité Args: batch_info (dict): Informations sur le lot - size: taille du lot - clicked: liens cliqués - deleted: emails supprimés - errors: erreurs rencontrées """ try: size = batch_info.get('size', 0) clicked = batch_info.get('clicked', 0) deleted = batch_info.get('deleted', 0) errors = batch_info.get('errors', 0) self.log_action("📊 " + "─" * 25 + " RÉSUMÉ LOT " + "─" * 25, auto_scroll=True) self.log_action(f"📧 Emails traités: {size}", auto_scroll=True) self.log_action(f"🔗 Liens cliqués: {clicked}", auto_scroll=True) self.log_action(f"🗑️ Emails supprimés: {deleted}", auto_scroll=True) if errors > 0: self.log_action(f"❌ Erreurs: {errors}", auto_scroll=True) else: self.log_action("✅ Aucune erreur", auto_scroll=True) self.log_action("📊 " + "─" * 56, auto_scroll=True) except Exception as e: self.log_action(f"❌ Erreur résumé lot: {str(e)}", auto_scroll=True) def log_cycle_info(self, cycle_num, emails_in_cycle, pause_time): """ Log d'informations sur un cycle Args: cycle_num (int): Numéro du cycle emails_in_cycle (int): Nombre d'emails dans ce cycle pause_time (float): Temps de pause après le cycle """ try: self.log_action("🔄 " + "═" * 20 + f" CYCLE {cycle_num} " + "═" * 20, auto_scroll=True) self.log_action(f"📧 Emails à traiter: {emails_in_cycle}", auto_scroll=True) self.log_action(f"⏱️ Pause après cycle: {pause_time:.1f} secondes", auto_scroll=True) self.log_action("🔄 " + "═" * 47, auto_scroll=True) except Exception as e: self.log_action(f"❌ Erreur log cycle: {str(e)}", auto_scroll=True) def log_final_summary(self, summary_data): """ Log du résumé final SANS défilement pour les décomptes Args: summary_data (dict): Données du résumé final """ try: self.log_action("🏁 " + "═" * 15 + " TRAITEMENT TERMINÉ " + "═" * 15, auto_scroll=True) total_processed = summary_data.get('total_processed', 0) total_clicked = summary_data.get('total_clicked', 0) total_deleted = summary_data.get('total_deleted', 0) total_errors = summary_data.get('total_errors', 0) doublons = summary_data.get('doublons', 0) self.log_action(f"📧 Total emails traités: {total_processed}", auto_scroll=True) self.log_action(f"🔗 Total liens cliqués: {total_clicked}", auto_scroll=True) self.log_action(f"🗑️ Total emails supprimés: {total_deleted}", auto_scroll=True) if doublons > 0: self.log_action(f"🔶 Doublons détectés: {doublons}", auto_scroll=True) if total_errors > 0: self.log_action(f"❌ Total erreurs: {total_errors}", auto_scroll=True) else: self.log_action("✅ Aucune erreur rencontrée", auto_scroll=True) # Calcul du taux de succès if total_processed > 0: success_rate = ((total_processed - total_errors) / total_processed) * 100 self.log_action(f"📈 Taux de succès: {success_rate:.1f}%", auto_scroll=True) self.log_action("🏁 " + "═" * 50, auto_scroll=True) except Exception as e: self.log_action(f"❌ Erreur résumé final: {str(e)}", auto_scroll=True) def setup_log_display(self): """ Configure l'affichage des logs """ # Configuration du QTextEdit pour les logs self.log_text.setReadOnly(True) self.log_text.setStyleSheet(""" QTextEdit { background-color: #1e1e1e; color: #ffffff; border: 1px solid #34495e; border-radius: 4px; padding: 10px; font-family: Consolas, monospace; } QScrollBar:vertical { background: #2d2d2d; width: 12px; margin: 0px; } QScrollBar::handle:vertical { background-color: #4a4a4a; min-height: 20px; border-radius: 6px; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { background: none; } """) # Définir une taille de police fixe pour une meilleure lisibilité font = QFont("Consolas", 8) self.log_text.setFont(font) def update_mini_browser(self, url): self.mini_browser.load(QUrl(url)) if len(self.current_urls) > 1 and not self.carousel_timer.isActive(): self.carousel_timer.start(self.outlook_handler.pause_time * 1000) def update_full_browser(self, url): self.browser_window.load_url(url, self.outlook_handler.pause_time) def rotate_carousel(self): if self.current_urls: self.carousel_index = (self.carousel_index + 1) % len(self.current_urls) self.mini_browser.load(QUrl(self.current_urls[self.carousel_index])) def show_mini_browser(self): self.browser_window.hide() self.mini_browser.show() if self.current_urls: self.mini_browser.load(QUrl(self.current_urls[-1])) def show_full_browser(self): # Utiliser directement ha2 précédemment créé self.browser_window.show() if self.current_urls: self.browser_window.load(self.current_urls[-1]) # Supprimer clear_tabs car c'est spécifique à BrowserWindow if self.current_urls: self.browser_window.load(self.current_urls[-1]) def validate_inputs_for_scan(self): if not self.mailbox_listbox.currentItem() or not self.folder_listbox.currentItem(): QMessageBox.warning(self, "Erreur", "Veuillez sélectionner une boîte mail et un dossier.") return False if not self.link_entry.text(): QMessageBox.warning(self, "Erreur", "Veuillez entrer des mots-clés pour le lien.") return False if not self.max_emails_entry.text(): QMessageBox.warning(self, "Erreur", "Veuillez entrer un nombre maximum d'emails à scanner.") return False if not self.max_emails_entry.text().isdigit(): QMessageBox.warning(self, "Erreur", "Le nombre maximum d'emails doit être un nombre entier positif.") return False return True def validate_inputs_for_count(self): if not self.mailbox_listbox.currentItem() or not self.folder_listbox.currentItem(): QMessageBox.warning(self, "Erreur", "Veuillez sélectionner une boîte mail et un dossier.") return False if not self.link_entry.text(): QMessageBox.warning(self, "Erreur", "Veuillez entrer des mots-clés pour le lien.") return False return True def customEvent(self, event): if event.type() == QEvent.User: url = event.data() self.update_mini_browser(url) def update_mailboxes(self): mailboxes = self.outlook_handler.get_mailboxes() self.mailbox_listbox.clear() self.mailbox_listbox.addItems(mailboxes) def on_mailbox_selected(self, item): selected_mailbox = item.text() self.update_folders(selected_mailbox) def update_folders(self, mailbox): """Met à jour l'affichage des dossiers en ne montrant que la boîte de réception et ses sous-dossiers""" try: folders = self.outlook_handler.get_folders(mailbox) self.folder_listbox.clear() # Stocker la liste complète des dossiers self.all_folders = folders # Afficher la structure des dossiers for folder_data in folders: item = QListWidgetItem() item.setText(folder_data['display_name']) item.setData(Qt.UserRole, { 'id': folder_data['id'], 'level': folder_data['level'], 'has_subfolders': folder_data['has_subfolders'], 'is_expanded': folder_data.get('is_expanded', False), 'name': folder_data['name'] }) # Cacher les sous-dossiers par défaut if folder_data['level'] > 0: item.setHidden(True) self.folder_listbox.addItem(item) self.folder_listbox.itemClicked.connect(self.toggle_folder) self.log_action(f"Liste des dossiers mise à jour pour {mailbox}") except Exception as e: self.log_action(f"Erreur lors de la mise à jour des dossiers : {str(e)}") def toggle_folder(self, item): """Gère le dépliage/repliage d'un dossier""" try: data = item.data(Qt.UserRole) if not data['has_subfolders']: return # Récupérer les informations actuelles current_level = data['level'] is_expanded = data['is_expanded'] # Inverser l'état d'expansion is_expanded = not is_expanded # Mettre à jour le symbole et l'état indent = ' ' * current_level new_symbol = '▼' if is_expanded else '▶' item.setText(f"{indent}{new_symbol} {data['name']}") # Mettre à jour les données de l'item data['is_expanded'] = is_expanded item.setData(Qt.UserRole, data) # Trouver l'index de l'élément actuel current_idx = self.folder_listbox.row(item) # Parcourir les éléments suivants jusqu'à trouver un élément de niveau égal ou supérieur idx = current_idx + 1 while idx < self.folder_listbox.count(): next_item = self.folder_listbox.item(idx) next_data = next_item.data(Qt.UserRole) if next_data['level'] <= current_level: break # Si on déplie, montrer uniquement les éléments du niveau immédiatement inférieur if is_expanded: next_item.setHidden(next_data['level'] > current_level + 1) else: next_item.setHidden(True) idx += 1 except Exception as e: self.log_action(f"Erreur lors du basculement du dossier : {str(e)}") print(f"Erreur détaillée: {str(e)}") def validate_reward(self): """Utilise le reward_entry de la section principale""" value = self.reward_entry.text().strip() # Utilise le champ de la section principale if not value: QMessageBox.warning(self, "Erreur de validation", "Le champ ne peut pas être vide.") return try: value = float(value.replace(',', '.')) if value <= 0: raise ValueError("La valeur doit être positive") self.outlook_handler.set_reward_value(value) self.reward_entry.setEnabled(False) # Désactive le champ récompense self.validate_reward_button.setEnabled(False) # Désactive le bouton valider self.reset_reward_button.setEnabled(True) # Active le bouton RAZ self.status_label.setText(f"Récompense validée : {value}") except ValueError as e: QMessageBox.warning(self, "Erreur de validation", f"Erreur : {str(e)}\nVeuillez entrer une valeur numérique positive valide.") def reset_reward(self): """Réinitialise le reward_entry de la section principale""" self.reward_entry.clear() # Efface le champ récompense self.reward_entry.setEnabled(True) # Réactive le champ self.validate_reward_button.setEnabled(True) # Réactive le bouton valider self.reset_reward_button.setEnabled(False) # Désactive le bouton RAZ self.outlook_handler.reset_reward_value() self.status_label.setText("Valeur de récompense réinitialisée") self.update_reward_display() def update_reward_display(self): """Met à jour l'affichage de la récompense""" try: if hasattr(self, 'outlook_handler') and hasattr(self.outlook_handler, 'total_reward'): reward = int(self.outlook_handler.total_reward) self.reward_display.setText(f"{reward:03d} ") except Exception as e: print(f"Erreur dans update_reward_display: {e}") finally: QApplication.processEvents() def update_font_size(self, size): """Met à jour la taille de police incluant le champ récompense""" self.font_size = size font = QFont() font.setPointSize(size) widgets_to_update = [ self.mailbox_listbox, self.folder_listbox, self.sender_entry, self.subject_entry, self.link_entry, self.reward_entry, # Inclut le champ récompense self.max_emails_entry ] for widget in widgets_to_update: widget.setFont(font) if self.outlook_handler: self.outlook_handler.set_font_size(size) def verifier_doublons(self): self.log_action("Début de la vérification des doublons basée sur les liens") doublons = [] links_hashes = {} try: pythoncom.CoInitialize() outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI") folder = outlook.Folders[self.mailbox_listbox.currentItem().text()].Folders[ self.folder_listbox.currentItem().text()] messages = folder.Items link_keywords = self.link_entry.text().split() self.log_action(f"Analyse du dossier : {folder.Name}") for message in messages: try: # Extraire le lien pertinent avec find_valid_link valid_link = self.outlook_handler.find_valid_link(message, link_keywords) if valid_link: # Utiliser uniquement le lien comme base du hash hash_key = hashlib.md5(valid_link.encode()).hexdigest() if hash_key in links_hashes: doublons.append((message, links_hashes[hash_key])) self.log_action(f"Doublon trouvé : {valid_link}") else: links_hashes[hash_key] = message except Exception as e: self.log_action(f"Erreur lors du traitement d'un message : {str(e)}") self.log_action(f"Vérification terminée. {len(doublons)} doublons trouvés.") return doublons except Exception as e: self.log_action(f"Erreur lors de la vérification des doublons : {str(e)}") return [] finally: pythoncom.CoUninitialize() def gerer_doublons(self, doublons): self.log_action("Début de la gestion des doublons") dialog = DoublonsDialog(self, doublons, self.settings.value("afficher_popup_toujours", False, type=bool)) result = dialog.exec_() if result == QDialog.Accepted: self.log_action("Paramètre 'afficher_popup_toujours' mis à jour") if doublons: self.log_action("Suppression des doublons acceptée") self.supprimer_doublons(doublons) return True else: self.log_action("Aucun doublon à supprimer") return False def afficher_popup_doublons(self, doublons): self.log_action("Affichage du popup de doublons") dialog = QDialog(self) dialog.setWindowTitle("Doublons détectés") layout = QVBoxLayout() label = QLabel(f"{len(doublons)} doublons trouvés. Suppression automatique dans 8 secondes...") layout.addWidget(label) list_widget = QListWidget() for doublon, original in doublons: list_widget.addItem(f"Doublon: {doublon.Subject} - Original: {original.Subject}") layout.addWidget(list_widget) button_layout = QHBoxLayout() cancel_button = QPushButton("Annuler") cancel_button.clicked.connect(dialog.reject) button_layout.addWidget(cancel_button) continue_button = QPushButton("Continuer") continue_button.clicked.connect(dialog.accept) button_layout.addWidget(continue_button) layout.addLayout(button_layout) dialog.setLayout(layout) timer = QTimer(dialog) timer.timeout.connect(dialog.accept) timer.start(8000) # 8 secondes result = dialog.exec_() if result == QDialog.Accepted: self.log_action("Suppression des doublons acceptée") self.supprimer_doublons(doublons) return True else: self.log_action("Suppression des doublons annulée") return False def supprimer_doublons(self, doublons): self.log_action("Début de la suppression des doublons") for doublon, _ in doublons: try: doublon.Delete() self.log_action(f"Doublon supprimé : {doublon.Subject}") except Exception as e: self.log_action(f"Erreur lors de la suppression du doublon {doublon.Subject}: {str(e)}") self.log_action("Fin de la suppression des doublons") # Vérification des doublons self.log_action("Vérification des doublons en cours...") doublons = self.verifier_doublons() if not self.gerer_doublons(doublons): self.log_action("Scan annulé.") return # Effacer le contenu du QTextEdit (afficheur) self.log_text.clear() self.log_action("Nouveau traitement démarré") self.is_scanning = True self.set_scanning_state(True) self.status_label.setText("Scan en cours...") self.progress_bar.setValue(0) mailbox = self.mailbox_listbox.currentItem().text() folder = self.folder_listbox.currentItem().text() sender = self.sender_entry.text() subject_keywords = self.subject_entry.text().split() link_keywords = self.link_entry.text().split() max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None add_debug_log("OutlookWidget.start_scan", f"Paramètres du scan - Mailbox: {mailbox}, Folder: {folder}, Sender: {sender}, Subject keywords: {subject_keywords}, Link keywords: {link_keywords}, Max emails: {max_emails}") self.outlook_handler.scan_progress.connect(self.update_progress) try: add_debug_log("OutlookWidget.start_scan", "Appel de scan_emails") clicked_links = self.outlook_handler.scan_emails(mailbox, folder, sender, subject_keywords, link_keywords, max_emails) self.log_action(f"Scan terminé. {clicked_links} liens cliqués.") add_debug_log("OutlookWidget.start_scan", f"Scan terminé. {clicked_links} liens cliqués.") if self.is_scanning: self.status_label.setText(f"Scan terminé. {clicked_links} liens cliqués.") self.update_reward_display() else: self.status_label.setText("Scan arrêté.") except Exception as e: self.log_action(f"Erreur lors du scan : {str(e)}") add_debug_log("OutlookWidget.start_scan", f"⚠️ Erreur lors du scan : {str(e)}") QMessageBox.critical(self, "Erreur", f"Une erreur s'est produite lors du scan : {str(e)}") self.status_label.setText("Erreur lors du scan") finally: self.outlook_handler.scan_progress.disconnect(self.update_progress) self.is_scanning = False self.set_scanning_state(False) self.progress_bar.setValue(100) self.time_display.setText("--:--") add_debug_log("OutlookWidget.start_scan", "Fin de start_scan") self.log_action( f"Paramètres du scan - Mailbox: {mailbox}, Folder: {folder}, Sender: {sender}, Subject keywords: {subject_keywords}, Link keywords: {link_keywords}, Max emails: {max_emails}") write_debug_file() def update_progress(self, value, current_count=None, max_emails=None, total_in_folder=None): try: # Mettre à jour la progression if hasattr(self, 'is_scanning') and self.is_scanning: if max_emails: progress = min(int((current_count or 0) / max_emails * 100), 100) self.progress_bar.setValue(progress) else: self.progress_bar.setValue(value) else: self.progress_bar.setValue(value) # Mettre à jour le texte de progression if hasattr(self, 'outlook_handler'): self.progress_bar.setFormat(f" %p% ") self.status_label.setText(f"Progression - {value}%") # Mettre à jour le compteur si toutes les informations sont disponibles if all(v is not None for v in [current_count, max_emails, total_in_folder]): self.update_email_counter(current_count, max_emails, total_in_folder) # Forcer la mise à jour de l'interface QApplication.processEvents() except Exception as e: print(f"Erreur dans update_progress: {str(e)}") logger.error(f"Erreur lors de la mise à jour de la progression: {str(e)}") def handle_scan_error(self, error_message): QMessageBox.critical(self, "Erreur", f"Une erreur s'est produite lors du scan : {error_message}") self.status_label.setText("Erreur lors du scan") self.finish_scan() def cleanup_emails(self): """ Interface améliorée pour le nettoyage des emails """ # Validation des entrées if not self.validate_inputs_for_count(): return # Demander confirmation avec plus de détails confirm_dialog = QMessageBox(self) confirm_dialog.setWindowTitle("🧹 Confirmation de nettoyage") confirm_dialog.setText("Comment voulez-vous nettoyer les emails ?") confirm_dialog.setDetailedText( "• 'Tous les emails' : Analyse tous les emails du dossier\n" "• 'Nombre indiqué' : Limite l'analyse au nombre spécifié\n" "• Les emails périmés seront SUPPRIMÉS définitivement" ) confirm_dialog.setIcon(QMessageBox.Question) all_button = confirm_dialog.addButton("Tous les emails", QMessageBox.YesRole) limited_button = confirm_dialog.addButton("Nombre indiqué", QMessageBox.NoRole) cancel_button = confirm_dialog.addButton("Annuler", QMessageBox.RejectRole) confirm_dialog.exec_() clicked_button = confirm_dialog.clickedButton() if clicked_button == cancel_button: self.log_action("🚫 Nettoyage annulé par l'utilisateur") return # Déterminer le nombre max d'emails max_emails = None if clicked_button == limited_button: try: max_emails = int(self.max_emails_entry.text()) if self.max_emails_entry.text() else None except ValueError: QMessageBox.warning(self, "Erreur", "Le nombre doit être un entier valide.") return # Préparation de l'interface self.is_counting = True self.status_label.setText("🧹 Nettoyage en cours...") self.set_scanning_state(True) self.start_count_animation() self.progress_bar.setValue(0) try: # Récupération des paramètres mailbox = self.mailbox_listbox.currentItem().text() folder = self.folder_listbox.currentItem().text() sender = self.sender_entry.text() subject_keywords = self.subject_entry.text().split() if self.subject_entry.text() else [] link_keywords = self.link_entry.text().split() # Vérification de la validité validity_days = None if self.validity_checkbox.isChecked() and self.validity_entry.text(): try: validity_days = int(self.validity_entry.text()) self.log_action(f"📅 Critère de validité: {validity_days} jours") except ValueError: QMessageBox.warning(self, "Erreur", "La validité doit être un nombre entier") return # Callback de progression def progress_handler(current, total): if total > 0: progress = int((current / total) * 100) self.progress_bar.setValue(progress) self.status_label.setText(f"🔍 Analyse: {current}/{total}") QApplication.processEvents() # APPEL DE LA NOUVELLE MÉTHODE result = self.outlook_handler.cleanup_expired_emails( mailbox, folder, sender, subject_keywords, link_keywords, max_emails, validity_days, progress_callback=progress_handler ) # AFFICHAGE DÉTAILLÉ DES RÉSULTATS if isinstance(result, dict): # Mise à jour de l'affichage principal deleted_count = result.get('deleted_count', 0) expired_count = result.get('expired_count', 0) valid_count = result.get('valid_count', 0) self.count_label.setText( f"✅ Valides: {valid_count} | " f"🗑️ Supprimés: {deleted_count}/{expired_count}" ) self.status_label.setText("🏁 Nettoyage terminé") self.progress_bar.setValue(100) # Boîte de dialogue de résumé détaillé summary_dialog = QMessageBox(self) summary_dialog.setWindowTitle("📊 Résumé du nettoyage") summary_dialog.setIcon(QMessageBox.Information) summary_text = f""" 🏁 NETTOYAGE TERMINÉ 📊 Résumé: - Emails analysés: {result.get('processed_count', 0)} - Emails valides conservés: {valid_count} - Emails périmés détectés: {expired_count} - Emails effectivement supprimés: {deleted_count} {f"❌ Erreurs de suppression: {len(result.get('deletion_errors', []))}" if result.get('deletion_errors') else "✅ Aucune erreur"} """ summary_dialog.setText(summary_text) # Détails des emails supprimés si demandé if result.get('expired_details'): details = "📋 Détails des emails supprimés:\n\n" for email in result['expired_details'][:5]: # Limiter à 5 pour l'affichage details += f"• {email['subject'][:40]}...\n" details += f" De: {email['sender']}\n" details += f" Âge: {email['age_days']} jours\n\n" if len(result['expired_details']) > 5: details += f"... et {len(result['expired_details']) - 5} autres emails" summary_dialog.setDetailedText(details) summary_dialog.exec_() except Exception as e: self.log_action(f"❌ Erreur lors du nettoyage: {str(e)}") self.status_label.setText("❌ Erreur lors du nettoyage") QMessageBox.critical(self, "Erreur", f"Erreur pendant le nettoyage:\n{str(e)}") finally: self.is_counting = False self.stop_count_animation() self.set_scanning_state(False) def start_count_animation(self): self.count_animation_step = 0 self.count_timer.start(300) # Met à jour toutes les 300 ms pour une animation plus rapide self.count_button.setStyleSheet(""" QPushButton { background-color: #FFA500; /* Orange pour plus de visibilité */ color: white; border: none; padding: 5px 10px; border-radius: 3px; font-weight: bold; } """) def stop_count_animation(self): self.count_timer.stop() self.count_button.setText("Nettoyer les emails") self.count_button.setStyleSheet(self.button_style) def update_count_button_text(self): dots = "." * (self.count_animation_step % 4) self.count_button.setText(f"Comptage{dots}") self.count_animation_step += 1 QApplication.processEvents() # Force la mise à jour de l'interface def set_scanning_state(self, is_active): print(f"Définition de l'état de scan: {is_active}") try: # Gérer séparément les différents groupes de boutons # Boutons qui doivent être désactivés pendant le scan (activés après) for button_name in ['start_button', 'count_button', 'reset_reload_button']: if hasattr(self, button_name): button = getattr(self, button_name) button.setEnabled(not is_active) # Désactivé pendant scan, activé après # Boutons qui doivent être activés pendant le scan (désactivés après) for button_name in ['stop_button', 'pause_button', 'skip_button']: if hasattr(self, button_name): button = getattr(self, button_name) button.setEnabled(is_active) # Activé pendant scan, désactivé après except Exception as e: print(f"Erreur dans set_scanning_state: {e}") def toggle_pause(self): """PAUSE SYSTÈME COMPLÈTE - Arrêt de TOUS les processus Python""" if self.is_scanning or self.is_counting: if self.pause_button.text() == "Pause": # === PAUSE SYSTÈME TOTALE === self.pause_button.setText("Reprendre") self.status_label.setText("🔴 PAUSE SYSTÈME - Page web libérée") self.log_action("🔴 ARRÊT COMPLET DE TOUS LES PROCESSUS") # 1. ARRÊTER TOUS LES TIMERS IMMÉDIATEMENT self._stop_all_timers() # 2. SUSPENDRE TOUS LES THREADS PYTHON self._suspend_all_threads() # 3. ARRÊTER TOUTES LES BOUCLES D'ÉVÉNEMENTS self._stop_event_loops() # 4. LIBÉRER TOUTE LA MÉMOIRE POSSIBLE self._force_memory_cleanup() # 5. ARRÊTER LE PROCESSING QT (sauf navigateur) self._minimize_qt_processing() # 6. SAUVEGARDER L'ÉTAT POUR LA REPRISE self._save_system_state() self.log_action("✅ SYSTÈME EN PAUSE - CPU/RAM libérés pour la page web") # === 🎯 AJOUTER ÇA JUSTE APRÈS === # Test pour détecter quand ça redémarre QTimer.singleShot(8000, lambda: self.log_action("⏰ 8 secondes écoulées")) QTimer.singleShot(12000, lambda: self.log_action("⏰ 12 secondes écoulées")) QTimer.singleShot(15000, lambda: self.log_action("⏰ 15 secondes écoulées")) QTimer.singleShot(20000, lambda: self.log_action("⏰ 20 secondes écoulées")) # === FIN TEST === else: # === REPRISE SYSTÈME === self.pause_button.setText("Pause") self.status_label.setText("🟢 SYSTÈME ACTIF") self.log_action("🔄 REDÉMARRAGE DE TOUS LES PROCESSUS") # 1. RESTAURER LES TIMERS self._restore_all_timers() # 2. REDÉMARRER LES THREADS self._resume_all_threads() # 3. RELANCER LES BOUCLES D'ÉVÉNEMENTS self._restart_event_loops() # 4. RESTAURER L'ÉTAT SYSTÈME self._restore_system_state() self.log_action("✅ SYSTÈME REDÉMARRÉ - Traitement normal") def _stop_all_timers(self): """Arrête TOUS les timers et bloque tous les automatismes""" try: # === 🛑 MARQUAGE GLOBAL DU SYSTÈME EN PAUSE === self._system_paused = True self._threads_suspended = True self.paused_timers_state = {} timers_stopped = 0 self.log_action("🔴 DÉBUT PAUSE SYSTÈME COMPLÈTE") # === 🛑 BLOCAGE DES MÉTHODES AUTOMATIQUES === # Sauvegarder et bloquer les méthodes critiques self._blocked_methods = {} methods_to_block = [ 'update_browser', 'process_batch', 'start_scan', 'scan_emails', 'process_next_email', 'continue_processing', 'finish_processing', 'update_time_display', 'update_progress', 'record_action' ] for method_name in methods_to_block: if hasattr(self, method_name): # Sauvegarder la méthode originale self._blocked_methods[method_name] = getattr(self, method_name) # La remplacer par une version bloquée def create_blocked_method(orig_name): return lambda *args, **kwargs: self.log_action(f"🛑 {orig_name} bloqué (pause active)") setattr(self, method_name, create_blocked_method(method_name)) # Bloquer aussi les méthodes du handler Outlook if hasattr(self, 'outlook_handler'): handler_methods = ['scan_emails', 'process_batch', 'update_links_clicked', 'safe_rpc_call'] for method_name in handler_methods: if hasattr(self.outlook_handler, method_name): self._blocked_methods[f'handler_{method_name}'] = getattr(self.outlook_handler, method_name) def create_handler_blocked_method(orig_name): return lambda *args, **kwargs: self.log_action(f"🛑 Handler.{orig_name} bloqué") setattr(self.outlook_handler, method_name, create_handler_blocked_method(method_name)) # === 🛑 ARRÊT BRUTAL DU TIMER WIDGET === if hasattr(self, 'timer_widget') and self.timer_widget: # Sauvegarder son état complet self.paused_timer_widget_state = { 'text': self.timer_widget.text() if hasattr(self.timer_widget, 'text') else "", 'current_mode': getattr(self.timer_widget, 'current_mode', 'waiting'), 'was_visible': self.timer_widget.isVisible(), 'was_enabled': self.timer_widget.isEnabled() } # L'arrêter complètement if hasattr(self.timer_widget, 'stop_all_internal_timers'): self.timer_widget.stop_all_internal_timers() # Couper tous les signaux/slots if hasattr(self.timer_widget, 'blockSignals'): self.timer_widget.blockSignals(True) # Afficher un message fixe de pause try: self.timer_widget.set_text("⏸️ SYSTÈME EN PAUSE", "paused") except: pass timers_stopped += 1 self.log_action("🛑 TimerWidget COMPLÈTEMENT arrêté") # === 🛑 ARRÊT DE TOUS LES TIMERS PYTHON === # Timer principal de scan if hasattr(self, 'scan_timer') and self.scan_timer.isActive(): self.paused_timers_state['scan_timer'] = { 'was_active': True, 'interval': self.scan_timer.interval(), 'remaining_time': getattr(self, 'remaining_time', 0) } self.scan_timer.stop() timers_stopped += 1 self.log_action(f"⏸️ Timer scan arrêté - Temps: {getattr(self, 'remaining_time', 0)}") # Tous les timers spécifiques timer_attributes = [ 'task_update_timer', 'count_timer', 'log_clear_timer', 'tip_animation_timer', 'tip_change_timer', 'tips_fade_timer', 'countdown_timer', 'summary_timer', 'resize_timer', 'panel_timer', 'page_timer', 'display_timer', 'reload_timer', 'process_timer' ] for timer_name in timer_attributes: if hasattr(self, timer_name): timer = getattr(self, timer_name) if hasattr(timer, 'isActive') and timer.isActive(): self.paused_timers_state[timer_name] = { 'was_active': True, 'interval': timer.interval() } timer.stop() timers_stopped += 1 # Timers du handler Outlook if hasattr(self, 'outlook_handler'): handler_timers = ['keep_alive_timer', 'auto_disconnect_timer'] for timer_name in handler_timers: if hasattr(self.outlook_handler, timer_name): timer = getattr(self.outlook_handler, timer_name) if hasattr(timer, 'isActive') and timer.isActive(): self.paused_timers_state[f'handler_{timer_name}'] = { 'was_active': True, 'interval': timer.interval() } timer.stop() timers_stopped += 1 # === 🛑 RECHERCHE ET ARRÊT DE TOUS LES TIMERS CACHÉS === from PyQt5.QtCore import QTimer import gc hidden_timers_stopped = 0 for obj in gc.get_objects(): if isinstance(obj, QTimer) and obj.isActive(): try: # Identifier le timer parent_info = str(type(obj.parent())) if obj.parent() else "No parent" timer_id = id(obj) # S'assurer qu'il n'est pas déjà traité if f'hidden_{timer_id}' not in self.paused_timers_state: self.paused_timers_state[f'hidden_{timer_id}'] = { 'was_active': True, 'interval': obj.interval(), 'timer_object': obj, 'parent_info': parent_info } obj.stop() hidden_timers_stopped += 1 except Exception as e: # Timer inaccessible, on continue pass if hidden_timers_stopped > 0: self.log_action(f"🛑 {hidden_timers_stopped} timers cachés arrêtés") # === 🛑 PROTECTION SPÉCIALE CONTRE RESTART RÉSEAU === # Bloquer processEvents pour limiter les redémarrages automatiques from PyQt5.QtCore import QCoreApplication if not hasattr(self, '_original_processEvents'): self._original_processEvents = QCoreApplication.processEvents # Version limitée qui traite seulement les événements critiques def limited_processEvents(): try: if not getattr(self, '_system_paused', False): self._original_processEvents() except: pass QCoreApplication.processEvents = limited_processEvents # === 🛑 MARQUER L'AFFICHAGE EN PAUSE === self.time_display_paused = True total_stopped = timers_stopped + hidden_timers_stopped self.log_action(f"🔴 PAUSE COMPLÈTE ACTIVÉE - {total_stopped} timers arrêtés") self.log_action("🌐 Navigateur reste ACTIF pour utilisation manuelle") except Exception as e: self.log_action(f"❌ Erreur arrêt système: {e}") import traceback self.log_action(f"Stack: {traceback.format_exc()[-200:]}") def _restore_all_timers(self): """Restaure tous les timers et automatismes""" try: # === 🔄 DÉMARQUAGE GLOBAL === self._system_paused = False self._threads_suspended = False self.time_display_paused = False self.log_action("🟢 DÉBUT REPRISE SYSTÈME COMPLÈTE") timers_restarted = 0 # === 🔄 RESTAURATION DES MÉTHODES BLOQUÉES === if hasattr(self, '_blocked_methods'): for method_name, original_method in self._blocked_methods.items(): try: if method_name.startswith('handler_'): # Méthode du handler real_method_name = method_name.replace('handler_', '') if hasattr(self.outlook_handler, real_method_name): setattr(self.outlook_handler, real_method_name, original_method) else: # Méthode normale if hasattr(self, method_name): setattr(self, method_name, original_method) except Exception as e: self.log_action(f"⚠️ Erreur restauration {method_name}: {e}") del self._blocked_methods self.log_action("🔄 Toutes les méthodes automatiques restaurées") # === 🔄 RESTAURATION DU TIMER WIDGET === if hasattr(self, 'paused_timer_widget_state'): try: state = self.paused_timer_widget_state # Réactiver les signaux if hasattr(self.timer_widget, 'blockSignals'): self.timer_widget.blockSignals(False) # Restaurer la visibilité et l'état self.timer_widget.setVisible(state['was_visible']) self.timer_widget.setEnabled(state['was_enabled']) # Restaurer le texte et le mode try: self.timer_widget.set_text(state['text'], state['current_mode']) except: # Si ça échoue, au moins remettre un état par défaut self.timer_widget.set_text("Reprise...", "waiting") # Redémarrer ses timers internes si nécessaire if hasattr(self.timer_widget, 'restart_internal_timers'): self.timer_widget.restart_internal_timers() del self.paused_timer_widget_state self.log_action("🔄 TimerWidget complètement restauré") except Exception as e: self.log_action(f"⚠️ Erreur restauration TimerWidget: {e}") # === 🔄 RESTAURATION DES TIMERS PYTHON === if hasattr(self, 'paused_timers_state'): # Timer principal en premier if 'scan_timer' in self.paused_timers_state: timer_state = self.paused_timers_state['scan_timer'] if timer_state['was_active'] and hasattr(self, 'scan_timer'): # Restaurer le temps restant exact self.remaining_time = timer_state.get('remaining_time', 0) if self.remaining_time > 0: self.scan_timer.start(timer_state['interval']) timers_restarted += 1 self.log_action(f"▶️ Timer scan restauré - Temps: {self.remaining_time}") # Tous les autres timers for timer_name, timer_state in self.paused_timers_state.items(): if timer_name == 'scan_timer': continue # Déjà fait if not timer_state.get('was_active', False): continue try: # Timer caché identifié par ID if timer_name.startswith('hidden_'): timer_obj = timer_state.get('timer_object') if timer_obj and hasattr(timer_obj, 'start'): timer_obj.start(timer_state['interval']) timers_restarted += 1 # Timer du handler elif timer_name.startswith('handler_'): real_timer_name = timer_name.replace('handler_', '') if hasattr(self.outlook_handler, real_timer_name): timer = getattr(self.outlook_handler, real_timer_name) if hasattr(timer, 'start'): timer.start(timer_state['interval']) timers_restarted += 1 # Timer normal elif hasattr(self, timer_name): timer = getattr(self, timer_name) if hasattr(timer, 'start'): timer.start(timer_state['interval']) timers_restarted += 1 except Exception as e: self.log_action(f"⚠️ Erreur restauration timer {timer_name}: {e}") # Nettoyer l'état sauvegardé del self.paused_timers_state # === 🔄 RESTAURATION DE PROCESSEVENT === if hasattr(self, '_original_processEvents'): from PyQt5.QtCore import QCoreApplication QCoreApplication.processEvents = self._original_processEvents del self._original_processEvents self.log_action(f"🟢 REPRISE COMPLÈTE - {timers_restarted} timers redémarrés") self.log_action("🚀 Tous les automatismes sont maintenant actifs") except Exception as e: self.log_action(f"❌ Erreur reprise système: {e}") import traceback self.log_action(f"Stack: {traceback.format_exc()[-200:]}") def _suspend_all_threads(self): """Suspend tous les threads Python actifs""" try: import threading import time # Marquer tous les threads pour suspension self._threads_suspended = True # Obtenir la liste des threads actifs active_threads = threading.active_count() # Attendre que les threads se mettent en pause time.sleep(0.1) self.log_action(f"🧵 {active_threads} threads suspendus") except Exception as e: self.log_action(f"❌ Erreur suspension threads: {e}") def _stop_event_loops(self): """Arrête toutes les boucles d'événements non-critiques""" try: from PyQt5.QtCore import QCoreApplication # Réduire drastiquement la fréquence de traitement des événements QCoreApplication.processEvents = lambda: None # Marquer l'arrêt des boucles self._event_loops_stopped = True self.log_action("🔄 Boucles d'événements suspendues") except Exception as e: self.log_action(f"❌ Erreur arrêt boucles: {e}") def _force_memory_cleanup(self): """Force le nettoyage complet de la mémoire""" try: import gc import psutil # Mesurer la mémoire avant process = psutil.Process() memory_before = process.memory_info().rss / 1024 / 1024 # Nettoyage agressif gc.collect() gc.collect() # Double nettoyage gc.collect() # Triple nettoyage pour être sûr # Vider les caches Python if hasattr(self, 'scan_log_history'): self.scan_log_history.clear() if hasattr(self, 'report_data'): self.report_data.clear() # Mesurer après memory_after = process.memory_info().rss / 1024 / 1024 memory_freed = memory_before - memory_after self.log_action(f"🧹 Mémoire libérée: {memory_freed:.1f} MB") except Exception as e: self.log_action(f"❌ Erreur nettoyage mémoire: {e}") def _minimize_qt_processing(self): """Réduit au minimum le traitement Qt""" try: from PyQt5.QtCore import QCoreApplication # Réduire la priorité du processus import psutil import os current_process = psutil.Process(os.getpid()) current_process.nice(psutil.BELOW_NORMAL_PRIORITY_CLASS if os.name == 'nt' else 10) # Sauvegarder l'ancien processEvents self._original_processEvents = QCoreApplication.processEvents # Remplacer par une version minimale def minimal_processEvents(): pass QCoreApplication.processEvents = minimal_processEvents self.log_action("⚡ Traitement Qt minimisé") except Exception as e: self.log_action(f"❌ Erreur minimisation Qt: {e}") def _save_system_state(self): """Sauvegarde l'état du système pour la reprise""" try: import time self.system_state = { 'remaining_time': getattr(self, 'remaining_time', 0), 'current_progress': self.progress_bar.value() if hasattr(self, 'progress_bar') else 0, 'is_scanning': self.is_scanning, 'is_counting': self.is_counting, 'pause_timestamp': time.time(), 'display_time_paused': True # Marquer que l'affichage est en pause } # Marquer globalement que le système est en pause self.time_display_paused = True self.log_action("💾 État système sauvegardé") except Exception as e: self.log_action(f"❌ Erreur sauvegarde état: {e}") def _resume_all_threads(self): """Redémarre tous les threads""" try: # Marquer les threads comme actifs self._threads_suspended = False import threading active_threads = threading.active_count() self.log_action(f"🧵 {active_threads} threads réactivés") except Exception as e: self.log_action(f"❌ Erreur reprise threads: {e}") def _restart_event_loops(self): """Redémarre les boucles d'événements""" try: from PyQt5.QtCore import QCoreApplication # Restaurer le processEvents original if hasattr(self, '_original_processEvents'): QCoreApplication.processEvents = self._original_processEvents # Restaurer la priorité normale import psutil import os current_process = psutil.Process(os.getpid()) current_process.nice(psutil.NORMAL_PRIORITY_CLASS if os.name == 'nt' else 0) self._event_loops_stopped = False self.log_action("🔄 Boucles d'événements restaurées") except Exception as e: self.log_action(f"❌ Erreur restart boucles: {e}") def _restore_system_state(self): """Restaure l'état du système""" try: if hasattr(self, 'system_state'): state = self.system_state # Restaurer les états self.remaining_time = state.get('remaining_time', 0) if hasattr(self, 'progress_bar'): self.progress_bar.setValue(state.get('current_progress', 0)) # Restaurer l'affichage du temps self.time_display_paused = False # Calculer le temps écoulé pendant la pause pause_duration = time.time() - state.get('pause_timestamp', 0) self.log_action(f"⏱️ Pause de {pause_duration:.1f} secondes") # Mettre à jour l'affichage du temps immédiatement if self.remaining_time > 0: minutes = int(self.remaining_time // 60) seconds = int(self.remaining_time % 60) self.time_display.setText(f"{minutes:02d}:{seconds:02d}") del self.system_state self.log_action("🔄 État système restauré") except Exception as e: self.log_action(f"❌ Erreur restauration état: {e}") def skip_current_email(self): if self.is_scanning: # Appeler la méthode existante pour ignorer l'email self.outlook_handler.skip_current_email() self.log_action("Email actuel ignoré") # Mettre à jour l'affichage de la récompense et des liens self.update_reward_display() self.update_links_clicked_display(self.outlook_handler.links_clicked) # Réduire le temps restant pour refléter l'élément sauté # Estimer le temps moyen par email (pause d'affichage + pause entre emails) if hasattr(self, 'remaining_time') and self.remaining_time > 0: # Calculer le temps moyen par email avg_display_time = (self.outlook_handler.min_pause_time + self.outlook_handler.max_pause_time) / 2 avg_between_time = ( self.outlook_handler.min_between_pages_time + self.outlook_handler.max_between_pages_time) / 2 time_per_email = avg_display_time + avg_between_time # Déduire ce temps du temps restant total (avec un minimum pour éviter les valeurs négatives) self.remaining_time = max(0, self.remaining_time - time_per_email) # Mettre à jour l'affichage immédiatement minutes = int(self.remaining_time // 60) seconds = int(self.remaining_time % 60) self.time_display.setText(f"{minutes:02d}:{seconds:02d}") # Si le temps restant atteint zéro, arrêter le timer if self.remaining_time <= 0: if hasattr(self, 'scan_timer'): self.scan_timer.stop() # Fonction pour afficher le rapport (à ajouter) def display_report(self): """Affiche le rapport de traitement""" try: if hasattr(self.main_window, 'report_toggle') and self.main_window.report_toggle.isChecked(): report_content = self.generate_report() if report_content: # Importer ReportDialog directement depuis main.py from main import ReportDialog dialog = ReportDialog(report_content, self.main_window) dialog.exec_() except Exception as e: self.log_action(f"Erreur lors de l'affichage du rapport : {str(e)}") def generate_report(self): """Génère le contenu du rapport complet avec statistiques par cycle""" # DÉBOGAGE : Log du nombre d'entrées if hasattr(self, 'report_data'): print(f"DEBUG generate_report: Nombre d'entrées dans report_data: {len(self.report_data)}") else: print("DEBUG generate_report: report_data n'existe pas") report = "RAPPORT DE TRAITEMENT\n" report += "=" * 50 + "\n\n" # En-tête avec date et heure current_time = QDateTime.currentDateTime().toString("dd/MM/yyyy HH:mm:ss") report += f"Date du rapport: {current_time}\n\n" if not hasattr(self, 'report_data') or not self.report_data: report += "Aucune donnée de traitement n'a été enregistrée.\n\n" return report # Section des emails traités report += "EMAILS TRAITÉS\n" report += "-" * 30 + "\n\n" # DÉBOGAGE : Analyser les données avant traitement print("DEBUG generate_report: Analyse des données:") for i, data in enumerate(self.report_data): print(f" {i + 1}: {data.get('source', 'N/A')} - {data.get('datetime', 'N/A')}") # SOLUTION SIMPLE : Utiliser directement report_data SANS déduplication total_processed = len(self.report_data) total_reward = 0 print(f"DEBUG generate_report: Total à afficher: {total_processed}") # Traitement de chaque email for idx, data in enumerate(self.report_data, 1): report += f"Email traité #{idx}\n" report += "-" * 20 + "\n" report += f"Source: {data.get('source', 'Inconnu')}\n" report += f"Titre: {data.get('subject', 'Titre non disponible')}\n" report += f"Date/Heure: {data.get('datetime', 'Date inconnue')}\n" report += f"Lien traité: {data.get('link', 'Lien non disponible')}\n" # Formatage des temps display_time = data.get('display_time', 'N/A') pause_time = data.get('pause_time', 'N/A') if isinstance(display_time, (int, float)): display_time = f"{display_time:.1f}" if isinstance(pause_time, (int, float)): pause_time = f"{pause_time:.1f}" report += f"Temps d'affichage: {display_time} secondes\n" report += f"Temps de pause: {pause_time} secondes\n" # Calcul de la récompense reward = data.get('reward', 0) if reward != 'N/A': try: reward_value = float(reward) total_reward += reward_value report += f"Récompense: {reward_value:.2f}\n" except (ValueError, TypeError): report += f"Récompense: {reward}\n" else: report += f"Récompense: N/A\n" report += "=" * 40 + "\n\n" # Section des statistiques globales report += "\nSTATISTIQUES GLOBALES\n" report += "-" * 30 + "\n" # Calculer les emails périmés expired_emails = len(getattr(self, 'deleted_emails', [])) valid_emails = total_processed - expired_emails report += f"Emails valides: {valid_emails}\n" report += f"Emails périmés: {expired_emails}\n" report += f"Total général: {total_processed}\n" report += f"Récompense totale: {total_reward:.2f}\n\n" # Section des statistiques par cycle (si disponible) if hasattr(self, 'cycle_stats') and self.cycle_stats: report += "\nSTATISTIQUES PAR CYCLE\n" report += "-" * 30 + "\n" total_cycles = len(self.cycle_stats) total_cycle_emails = 0 for cycle_num, stats in enumerate(self.cycle_stats, 1): emails_processed = stats.get('emails_processed', 0) total_cycle_emails += emails_processed report += f"Cycle #{cycle_num}\n" report += f" Nombre d'emails traités: {emails_processed}\n" # Formatage du temps de pause entre cycles pause_between_cycles = stats.get('pause_between_cycles', 'N/A') if isinstance(pause_between_cycles, (int, float)): pause_between_cycles = f"{pause_between_cycles:.1f}" report += f" Temps de pause après le cycle: {pause_between_cycles} secondes\n" report += f" Heure de début: {stats.get('start_time', 'N/A')}\n" report += f" Heure de fin: {stats.get('end_time', 'N/A')}\n" # Calcul de la durée totale du cycle if 'start_time' in stats and 'end_time' in stats: try: start = QDateTime.fromString(stats['start_time'], "dd/MM/yyyy HH:mm:ss") end = QDateTime.fromString(stats['end_time'], "dd/MM/yyyy HH:mm:ss") if start.isValid() and end.isValid(): duration_secs = start.secsTo(end) minutes = duration_secs // 60 seconds = duration_secs % 60 report += f" Durée totale: {minutes} min {seconds} sec\n" except Exception: report += f" Durée totale: Calcul impossible\n" report += "-" * 20 + "\n" # Statistiques globales des cycles avg_emails_per_cycle = total_cycle_emails / total_cycles if total_cycles > 0 else 0 report += f"\nRésumé des cycles:\n" report += f"Nombre total de cycles: {total_cycles}\n" report += f"Total d'emails traités en cycles: {total_cycle_emails}\n" report += f"Moyenne d'emails par cycle: {avg_emails_per_cycle:.1f}\n\n" # Section des emails périmés détaillés (si présents) if hasattr(self, 'deleted_emails') and self.deleted_emails: report += "\nDÉTAIL DES EMAILS PÉRIMÉS\n" report += "-" * 30 + "\n" for idx, email in enumerate(self.deleted_emails, 1): report += f"Email périmé #{idx}\n" report += f"Sujet: {email.get('subject', 'Sujet inconnu')}\n" report += f"Expéditeur: {email.get('sender', 'Expéditeur inconnu')}\n" report += f"Date de réception: {email.get('received_date', 'Date inconnue')}\n" report += f"Âge: {email.get('age_days', 'N/A')} jours\n" report += "-" * 20 + "\n" # Pied de rapport report += "\nFin du rapport" report += "\n" + "=" * 50 + "\n" return report # Fonction pour enregistrer les actions def record_action(self, source, link, message=None): """ Enregistre une action pour le rapport en évitant les doublons """ try: if not hasattr(self.main_window, 'report_toggle') or not self.main_window.report_toggle.isChecked(): return import psutil # Récupérer la date de réception received_date = message.ReceivedTime.strftime("%Y-%m-%d %H:%M:%S") if message and hasattr(message, 'ReceivedTime') else QDateTime.currentDateTime().toString( "yyyy-MM-dd HH:mm:ss") # Récupérer le titre du mail subject = message.Subject if message and hasattr(message, 'Subject') else "Titre non disponible" # Créer une clé unique pour cette action action_key = f"{source}_{received_date}_{link}" # DÉBOGAGE : Log de la clé générée print(f"DEBUG record_action: Clé générée = {action_key}") # Vérifier si cette action a déjà été enregistrée if not hasattr(self, 'processed_actions'): self.processed_actions = set() if action_key not in self.processed_actions: # Enregistrer l'action uniquement si elle n'est pas déjà traitée action_data = { 'source': source, 'datetime': received_date, 'link': link, 'subject': subject, 'reward': self.outlook_handler.reward_value if hasattr(self.outlook_handler, 'reward_value') else 'N/A', 'cpu_usage': psutil.cpu_percent(), 'ram_usage': psutil.virtual_memory().percent, 'display_time': round(self.outlook_handler.display_time, 1), 'pause_time': round(self.outlook_handler.between_time, 1) } if not hasattr(self, 'report_data'): self.report_data = [] self.report_data.append(action_data) self.processed_actions.add(action_key) # DÉBOGAGE : Log du nombre d'actions print(f"DEBUG record_action: Action ajoutée. Total report_data: {len(self.report_data)}") self.log_action(f"Action enregistrée: {source} - Date: {received_date}") else: # DÉBOGAGE : Log des doublons détectés print(f"DEBUG record_action: DOUBLON DÉTECTÉ pour clé: {action_key}") except Exception as e: self.log_action(f"Erreur lors de l'enregistrement de l'action: {str(e)}") def update_stats_display(self, min_value=None, max_value=None): """Updates the statistics display in the existing statistics window Args: min_value: Optional minimum value (ignored for folder stats) max_value: Optional maximum value (ignored for folder stats) """ try: if not hasattr(self, 'folder_listbox') or not self.folder_listbox.currentItem(): return folder_data = self.folder_listbox.currentItem().data(Qt.UserRole) if not folder_data or 'id' not in folder_data: return folder_id = folder_data['id'] # Get stats with error handling try: if not hasattr(self, 'folder_stats'): self.folder_stats = FolderStats(self.outlook_handler) stats = self.folder_stats.get_folder_stats(folder_id) if not stats: return # Mettre à jour le texte des statistiques directement dans le widget messages_text = f"Messages totaux: {stats['total_messages']}\n" messages_text += f"Messages non lus: {stats['unread_messages']}\n" messages_text += f"Taux de lecture: {((stats['total_messages'] - stats['unread_messages']) / stats['total_messages'] * 100 if stats['total_messages'] > 0 else 0):.1f}%\n" messages_text += f"Premier message: {stats['time_stats']['oldest_message']}\n" messages_text += f"Dernier message: {stats['time_stats']['newest_message']}\n" if hasattr(self, 'count_label'): self.count_label.setText(messages_text) # Log success if hasattr(self, 'log_action'): self.log_action(f"Statistiques mises à jour pour {stats['folder_name']}") except Exception as e: logger.error(f"Error getting folder stats: {str(e)}") if hasattr(self, 'log_action'): self.log_action(f"Error getting folder statistics: {str(e)}") if hasattr(self, 'count_label'): self.count_label.setText("Erreur lors de la récupération des statistiques") except Exception as e: logger.error(f"Error updating stats display: {str(e)}") # Log error but continue execution class CyclePauseTimer(QFrame): """Widget pour afficher le temps de pause entre les cycles""" def __init__(self, parent=None): super().__init__(parent) self.setObjectName("cyclePauseTimer") self.setFixedHeight(60) # Hauteur fixe pour correspondre aux autres indicateurs # Style avec des couleurs qui correspondent à l'interface existante self.setStyleSheet(""" #cyclePauseTimer { background-color: #1a2a3a; border-radius: 4px; padding: 4px; } QLabel { color: white; font-size: 12px; } QLabel#timerValue { color: #65ddfb; font-size: 16px; font-weight: bold; } """) # Layout vertical layout = QVBoxLayout(self) layout.setSpacing(1) layout.setContentsMargins(2, 2, 2, 2) # Titre title_label = QLabel("⏱️ Pause") title_label.setAlignment(Qt.AlignCenter) layout.addWidget(title_label) # Valeur du timer self.timer_value = QLabel("00:00") self.timer_value.setObjectName("timerValue") self.timer_value.setAlignment(Qt.AlignCenter) layout.addWidget(self.timer_value) # Timer pour le décompte self.timer = QTimer(self) self.timer.timeout.connect(self.update_timer) self.remaining_seconds = 0 self.next_cycle = 0 # Masquer initialement self.hide() def start_countdown(self, seconds, next_cycle): """Démarre le compte à rebours""" self.remaining_seconds = max(1, int(seconds)) self.next_cycle = next_cycle self.update_display() self.show() self.timer.start(1000) # Mise à jour chaque seconde def update_timer(self): """Met à jour le timer à chaque seconde""" self.remaining_seconds -= 1 if self.remaining_seconds <= 0: self.timer.stop() self.hide() else: self.update_display() def update_display(self): """Met à jour l'affichage du temps restant""" minutes = self.remaining_seconds // 60 seconds = self.remaining_seconds % 60 self.timer_value.setText(f"{minutes:02d}:{seconds:02d}") # Changer la couleur en fonction du temps restant if self.remaining_seconds <= 10: self.timer_value.setStyleSheet("color: #FF5252; font-size: 16px; font-weight: bold;") # Rouge elif self.remaining_seconds <= 30: self.timer_value.setStyleSheet("color: #FFD740; font-size: 16px; font-weight: bold;") # Jaune else: self.timer_value.setStyleSheet("color: #65ddfb; font-size: 16px; font-weight: bold;") # Bleu class TimerWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) self.parent = parent self.setup_ui() # État du timer self.timer_running = False self.current_mode = "waiting" # "waiting", "display" ou "pause" self.remaining_time = 0.0 self.initial_time = 0.0 # Timer pour le décompte self.countdown_timer = QTimer(self) self.countdown_timer.timeout.connect(self.update_countdown) self.countdown_timer.setInterval(100) # Mise à jour toutes les 100ms self.timer_paused = False self.paused_time = 0.0 def setup_ui(self): """Crée l'interface utilisateur de l'afficheur timer""" layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(4) layout.setAlignment(Qt.AlignCenter) # Afficheur du timer self.timer_display = QLabel("----") self.timer_display.setFixedSize(90, 24) self.timer_display.setStyleSheet(""" background-color: #000000; color: #FFFFFF; font-family: Consolas; font-size: 12px; padding: 2px 5px; border-radius: 3px; qproperty-alignment: AlignCenter; """) layout.addWidget(self.timer_display) def set_text(self, text, mode="waiting"): """ Méthode utilisée par process_batch pour définir le texte et lancer le décompte. Gère les modes "waiting", "display", "pause". Empêche le démarrage du timer pendant un chargement de profil. """ # Arrêter l'ancien timer s'il est en cours self.countdown_timer.stop() # Définir le mode self.current_mode = mode # TRAITEMENT SPÉCIAL pour le mode "waiting" if mode == "waiting": self.timer_running = False self.remaining_time = 0.0 self.initial_time = 0.0 self.timer_display.setText(text) self.timer_display.setStyleSheet(""" background-color: #000000; color: #FFFFFF; font-family: Consolas; font-size: 12px; padding: 2px 5px; border-radius: 3px; qproperty-alignment: AlignCenter; """) return # Pour les modes "display" et "pause" self.timer_running = True # Extraire le temps du texte si possible try: time_value = float(text.split()[-1].replace("s", "")) self.initial_time = time_value self.remaining_time = time_value except (ValueError, IndexError): self.initial_time = 0.0 self.remaining_time = 0.0 # Définir la couleur et le préfixe if mode == "display": color = "#00FF00" prefix = "👁️" elif mode == "pause": color = "#FF6B6B" prefix = "⏲️" else: color = "#FFFFFF" prefix = "" # Mettre à jour l'affichage if self.remaining_time > 0: self.timer_display.setText(f"{prefix} {int(self.remaining_time)}s") self.timer_display.setStyleSheet(f""" background-color: #000000; color: {color}; font-family: Consolas; font-size: 12px; padding: 2px 5px; border-radius: 3px; qproperty-alignment: AlignCenter; """) # ✅ Bloquer le démarrage du timer pendant le chargement d'un profil if hasattr(self.parent, 'is_loading_profile') and self.parent.is_loading_profile: print(f"⚠️: Démarrage du timer bloqué (chargement profil) - text='{text}', mode='{mode}'") else: self.countdown_timer.start() else: self.timer_display.setText(text) self.timer_display.setStyleSheet(f""" background-color: #000000; color: {color}; font-family: Consolas; font-size: 12px; padding: 2px 5px; border-radius: 3px; qproperty-alignment: AlignCenter; """) def pause_timer(self): """Met en pause le timer""" if self.timer_running: self.countdown_timer.stop() self.timer_paused = True # Sauvegarder l'état actuel pour permettre une reprise ultérieure self.paused_time = self.remaining_time # Optionnellement, changer la couleur ou ajouter (PAUSE) au texte current_text = self.timer_display.text() self.timer_display.setText(f"{current_text} (PAUSE)") self.timer_display.setStyleSheet(""" background-color: #000000; color: #FF6B6B; /* Rouge pour indiquer la pause */ font-family: Consolas; font-size: 12px; padding: 2px 5px; border-radius: 3px; qproperty-alignment: AlignCenter; """) def resume_timer(self): """Reprend le timer à partir du point où il a été mis en pause""" if hasattr(self, 'timer_paused') and self.timer_paused: self.timer_paused = False self.remaining_time = self.paused_time # Restaurer l'affichage normal (enlever le texte PAUSE) if self.current_mode == "display": self.timer_display.setText(f"👁️ {int(self.remaining_time)}s") color = "#00FF00" # Vert pour le mode display elif self.current_mode == "pause": self.timer_display.setText(f"⏲️ {int(self.remaining_time)}s") color = "#FF6B6B" # Rouge pour le mode pause else: self.timer_display.setText("----") color = "#FFFFFF" # Blanc pour le mode waiting # Restaurer le style normal self.timer_display.setStyleSheet(f""" background-color: #000000; color: {color}; font-family: Consolas; font-size: 12px; padding: 2px 5px; border-radius: 3px; qproperty-alignment: AlignCenter; """) # Redémarrer le timer si on était en mode d'affichage if self.current_mode in ["display", "pause"] and self.remaining_time > 0: self.countdown_timer.start() self.timer_running = True def update_countdown(self): """Met à jour le décompte du timer SANS faire défiler le journal""" if not self.timer_running: return self.remaining_time = max(0, self.remaining_time - 0.1) # Mettre à jour l'affichage en fonction du mode if self.current_mode == "display": self.timer_display.setText(f"👁️ {int(self.remaining_time)}s") elif self.current_mode == "pause": self.timer_display.setText(f"⏲️ {int(self.remaining_time)}s") # Si le timer atteint 0 if self.remaining_time <= 0: self.countdown_timer.stop() self.timer_running = False if self.current_mode == "pause": self.current_mode = "waiting" self.timer_display.setText("----") self.timer_display.setStyleSheet(""" background-color: #000000; color: #FFFFFF; font-family: Consolas; font-size: 12px; padding: 2px 5px; border-radius: 3px; qproperty-alignment: AlignCenter; """) def reset_to_waiting(self): """Remet le timer en mode attente""" self.countdown_timer.stop() self.timer_running = False self.current_mode = "waiting" self.remaining_time = 0.0 self.timer_display.setText("----") self.timer_display.setStyleSheet(""" background-color: #000000; color: #FFFFFF; font-family: Consolas; font-size: 12px; padding: 2px 5px; border-radius: 3px; qproperty-alignment: AlignCenter; """) class FlipBanner(QLabel): def __init__(self, parent=None): super().__init__(parent) self.setFixedSize(468, 120) # Charger le GIF gif_path = os.path.join(BANNER_PATH, "EzClixA468x60.jpg") self.gif = QMovie(gif_path) self.gif.setScaledSize(QSize(468, 60)) self.setMovie(self.gif) self.gif.start() # Configuration de l'animation self.current_angle = 0 self.animation = QPropertyAnimation(self, b"rotation") self.animation.setDuration(1500) # 1.5 secondes par cycle self.animation.setStartValue(0) self.animation.setEndValue(180) self.animation.setEasingCurve(QEasingCurve.Linear) self.animation.finished.connect(self.restart_animation) self.animation.start() def restart_animation(self): # Inverser la direction start_value = self.animation.startValue() end_value = self.animation.endValue() self.animation.setStartValue(end_value) self.animation.setEndValue(start_value) self.animation.start() def get_rotation(self): return self.current_angle def set_rotation(self, angle): self.current_angle = angle self.update() def paintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.SmoothPixmapTransform) # Configurer la transformation painter.translate(self.width() / 2, self.height() / 2) painter.rotate(self.current_angle) painter.translate(-self.width() / 2, -self.height() / 2) # Dessiner l'image super().paintEvent(event) # Définir la propriété pour l'animation rotation = pyqtProperty(float, get_rotation, set_rotation) class ProcessedLinksHandler: def __init__(self, storage_path="C:/AUTOLOOK/processed_links.json"): self.storage_path = storage_path self.processed_links = self._load_processed_links() self._ensure_directory_exists() def _ensure_directory_exists(self): """S'assure que le répertoire de stockage existe""" directory = os.path.dirname(self.storage_path) if not os.path.exists(directory): os.makedirs(directory) def _load_processed_links(self): """Charge les liens traités depuis le fichier JSON""" try: if os.path.exists(self.storage_path): with open(self.storage_path, 'r') as f: return json.load(f) return {} except Exception as e: print(f"Erreur lors du chargement des liens traités: {e}") return {} def _save_processed_links(self): """Sauvegarde les liens traités dans le fichier JSON""" try: with open(self.storage_path, 'w') as f: json.dump(self.processed_links, f, indent=4) except Exception as e: print(f"Erreur lors de la sauvegarde des liens traités: {e}") def _generate_link_hash(self, link): """Génère un hash unique pour un lien""" return hashlib.md5(link.encode()).hexdigest() def is_link_processed(self, link): """Vérifie si un lien a déjà été traité""" link_hash = self._generate_link_hash(link) return link_hash in self.processed_links def add_processed_link(self, link, metadata=None): """Ajoute un lien traité avec ses métadonnées""" link_hash = self._generate_link_hash(link) self.processed_links[link_hash] = { 'link': link, 'processed_date': datetime.now().isoformat(), 'metadata': metadata or {} } self._save_processed_links() def get_link_info(self, link): """Récupère les informations d'un lien traité""" link_hash = self._generate_link_hash(link) return self.processed_links.get(link_hash) def clear_old_links(self, days=30): """Supprime les liens plus anciens qu'un certain nombre de jours""" current_time = datetime.now() links_to_remove = [] for link_hash, data in self.processed_links.items(): processed_date = datetime.fromisoformat(data['processed_date']) age = (current_time - processed_date).days if age > days: links_to_remove.append(link_hash) for link_hash in links_to_remove: del self.processed_links[link_hash] if links_to_remove: self._save_processed_links() return len(links_to_remove) return 0 def get_statistics(self): """Retourne des statistiques sur les liens traités""" return { 'total_links': len(self.processed_links), 'oldest_link': min((data['processed_date'] for data in self.processed_links.values()), default=None), 'newest_link': max((data['processed_date'] for data in self.processed_links.values()), default=None) } class FolderMonitor(QObject): folder_changed = pyqtSignal(str, dict) # Signal émis avec l'ID du dossier et les changements folder_error = pyqtSignal(str, str) # Signal émis en cas d'erreur def __init__(self, outlook_handler): super().__init__() self.outlook_handler = outlook_handler self.monitored_folders = {} # {folder_id: last_state} self.timer = QTimer() self.timer.timeout.connect(self.check_folders) self.monitoring_active = False self.check_interval = 30000 # 30 secondes par défaut def start_monitoring(self, folder_id): """Démarre la surveillance d'un dossier spécifique""" try: initial_state = self.get_folder_state(folder_id) self.monitored_folders[folder_id] = initial_state if not self.monitoring_active: self.timer.start(self.check_interval) self.monitoring_active = True return True except Exception as e: self.folder_error.emit(folder_id, str(e)) return False def stop_monitoring(self, folder_id=None): """Arrête la surveillance d'un dossier ou de tous les dossiers""" if folder_id: self.monitored_folders.pop(folder_id, None) if not self.monitored_folders: self.timer.stop() self.monitoring_active = False else: self.monitored_folders.clear() self.timer.stop() self.monitoring_active = False def get_folder_state(self, folder_id): """Récupère l'état actuel d'un dossier""" try: folder = self.outlook_handler.get_folder_by_id(folder_id) return { 'total_items': folder.Items.Count, 'unread_items': len([item for item in folder.Items if not item.UnRead]), 'last_modification': folder.Items.Sort("[ReceivedTime]")[ 0].ReceivedTime if folder.Items.Count > 0 else None, 'size': sum(item.Size for item in folder.Items) } except Exception as e: self.folder_error.emit(folder_id, f"Erreur lors de la récupération de l'état: {str(e)}") return None def check_folders(self): """Vérifie les changements dans tous les dossiers surveillés""" for folder_id, last_state in list(self.monitored_folders.items()): try: current_state = self.get_folder_state(folder_id) if current_state and self.detect_changes(last_state, current_state): changes = self.compute_changes(last_state, current_state) self.folder_changed.emit(folder_id, changes) self.monitored_folders[folder_id] = current_state except Exception as e: self.folder_error.emit(folder_id, str(e)) def detect_changes(self, old_state, new_state): """Détecte s'il y a eu des changements entre deux états""" if not old_state or not new_state: return False return any(old_state[key] != new_state[key] for key in old_state) def compute_changes(self, old_state, new_state): """Calcule les changements précis entre deux états""" changes = {} for key in old_state: if old_state[key] != new_state[key]: changes[key] = { 'old': old_state[key], 'new': new_state[key], 'diff': new_state[key] - old_state[key] if isinstance(old_state[key], (int, float)) else None } return changes class FolderStats: def __init__(self, outlook_handler): self.outlook_handler = outlook_handler self.stats_cache = {} self.cache_timeout = 300 # 5 minutes def get_folder_stats(self, folder_id, force_refresh=False): """Récupère les statistiques complètes d'un dossier""" current_time = time.time() if not force_refresh and folder_id in self.stats_cache: cached_time, stats = self.stats_cache[folder_id] if current_time - cached_time < self.cache_timeout: return stats try: folder = self.outlook_handler.get_folder_by_id(folder_id) stats = self._calculate_folder_stats(folder) self.stats_cache[folder_id] = (current_time, stats) return stats except Exception as e: print(f"Erreur lors du calcul des statistiques: {str(e)}") return None def _calculate_folder_stats(self, folder): """Calcule toutes les statistiques pour un dossier""" items = folder.Items total_items = items.Count if total_items == 0: return self._empty_folder_stats(folder.Name) items.Sort("[ReceivedTime]") stats = { 'folder_name': folder.Name, 'total_messages': total_items, 'unread_messages': len([item for item in items if not item.UnRead]), 'time_stats': self._calculate_time_stats(items), 'size_stats': self._calculate_size_stats(items), 'sender_stats': self._calculate_sender_stats(items), 'category_stats': self._calculate_category_stats(items), } return stats def _calculate_time_stats(self, items): """Calcule les statistiques temporelles complètes""" try: if items.Count == 0: return { 'oldest_message': None, 'newest_message': None, 'messages_by_month': {}, 'messages_by_day': {} } oldest = None newest = None messages_by_month = {} messages_by_day = {} for item in items: received_time = item.ReceivedTime if not received_time: continue # Mise à jour du plus ancien/récent if not oldest or received_time < oldest: oldest = received_time if not newest or received_time > newest: newest = received_time # Groupement par mois month_key = received_time.strftime("%Y-%m") messages_by_month[month_key] = messages_by_month.get(month_key, 0) + 1 # Groupement par jour day_key = received_time.strftime("%Y-%m-%d") messages_by_day[day_key] = messages_by_day.get(day_key, 0) + 1 return { 'oldest_message': oldest, 'newest_message': newest, 'messages_by_month': dict(sorted(messages_by_month.items())), 'messages_by_day': dict(sorted(messages_by_day.items())) } except Exception as e: print(f"Erreur lors du calcul des statistiques temporelles : {str(e)}") return { 'oldest_message': None, 'newest_message': None, 'messages_by_month': {}, 'messages_by_day': {} } def _calculate_size_stats(self, items): """Calcule les statistiques de taille""" try: total_size = 0 max_size = 0 count = 0 for item in items: size = getattr(item, 'Size', 0) total_size += size max_size = max(max_size, size) count += 1 return { 'total_size': total_size, 'average_size': total_size / count if count > 0 else 0, 'max_size': max_size } except Exception as e: print(f"Erreur lors du calcul des statistiques de taille : {str(e)}") return { 'total_size': 0, 'average_size': 0, 'max_size': 0 } def _calculate_sender_stats(self, items): """Calcule les statistiques par expéditeur""" try: sender_stats = {} for item in items: sender = getattr(item, 'SenderEmailAddress', 'Unknown') sender_stats[sender] = sender_stats.get(sender, 0) + 1 return dict(sorted(sender_stats.items(), key=lambda x: x[1], reverse=True)) except Exception as e: print(f"Erreur lors du calcul des statistiques d'expéditeur : {str(e)}") return {} def _calculate_category_stats(self, items): """Calcule les statistiques par catégorie""" try: category_stats = {} for item in items: categories = getattr(item, 'Categories', '').split(', ') for category in categories: if category: category_stats[category] = category_stats.get(category, 0) + 1 return dict(sorted(category_stats.items(), key=lambda x: x[1], reverse=True)) except Exception as e: print(f"Erreur lors du calcul des statistiques de catégorie : {str(e)}") return {} def _group_by_month(self, items): """Groupe les messages par mois""" try: monthly_counts = {} for item in items: received_time = item.ReceivedTime if received_time: month_key = received_time.strftime("%Y-%m") monthly_counts[month_key] = monthly_counts.get(month_key, 0) + 1 return dict(sorted(monthly_counts.items())) except Exception as e: print(f"Erreur lors du groupement par mois : {str(e)}") return {} def _group_by_day(self, items): """Groupe les messages par jour""" try: daily_counts = {} for item in items: received_time = item.ReceivedTime if received_time: day_key = received_time.strftime("%Y-%m-%d") daily_counts[day_key] = daily_counts.get(day_key, 0) + 1 return dict(sorted(daily_counts.items())) except Exception as e: print(f"Erreur lors du groupement par jour : {str(e)}") return {} def generate_report(self, folder_id): """Génère un rapport détaillé des statistiques""" stats = self.get_folder_stats(folder_id) if not stats: return "Impossible de générer le rapport : statistiques non disponibles" report = f""" Rapport de statistiques pour le dossier {stats['folder_name']} ======================================================== Informations générales : - Nombre total de messages : {stats['total_messages']} - Messages non lus : {stats['unread_messages']} - Taux de lecture : {((stats['total_messages'] - stats['unread_messages']) / stats['total_messages'] * 100):.1f}% Distribution temporelle : - Message le plus ancien : {stats['time_stats']['oldest_message']} - Message le plus récent : {stats['time_stats']['newest_message']} Statistiques des expéditeurs (Top 5) : {self._format_sender_stats(stats['sender_stats'])} Distribution par catégorie : {self._format_category_stats(stats['category_stats'])} Analyse de la taille : - Taille totale : {self._format_size(stats['size_stats']['total_size'])} - Taille moyenne : {self._format_size(stats['size_stats']['average_size'])} - Plus grand message : {self._format_size(stats['size_stats']['max_size'])} """ return report def _format_size(self, size_in_bytes): """Formate une taille en bytes en format lisible""" for unit in ['B', 'KB', 'MB', 'GB']: if size_in_bytes < 1024: return f"{size_in_bytes:.1f} {unit}" size_in_bytes /= 1024 return f"{size_in_bytes:.1f} TB" def _format_sender_stats(self, sender_stats): """Formate les statistiques des expéditeurs""" return "\n".join([f"- {sender}: {count} messages" for sender, count in sorted(sender_stats.items(), key=lambda x: x[1], reverse=True)[:5]]) def _empty_folder_stats(self, folder_name): """Retourne des statistiques par défaut pour un dossier vide""" return { 'folder_name': folder_name, 'total_messages': 0, 'unread_messages': 0, 'time_stats': {'oldest_message': None, 'newest_message': None}, 'size_stats': {'total_size': 0, 'average_size': 0, 'max_size': 0}, 'sender_stats': {}, 'category_stats': {} } class DoublonsDialog(QDialog): def __init__(self, parent, doublons, afficher_toujours): super().__init__(parent) self.doublons = doublons self.setWindowTitle("Vérification des doublons") self.setLayout(QVBoxLayout()) if doublons: self.layout().addWidget(QLabel(f"{len(doublons)} doublons trouvés.")) list_widget = QListWidget() for doublon, original in doublons: list_widget.addItem(f"Doublon: {doublon.Subject} - Original: {original.Subject}") self.layout().addWidget(list_widget) else: self.layout().addWidget(QLabel("Aucun doublon trouvé.")) button_layout = QHBoxLayout() self.cancel_button = QPushButton("Annuler") self.cancel_button.clicked.connect(self.reject) button_layout.addWidget(self.cancel_button) self.timer_label = QLabel("Suppression automatique dans 8 secondes...") button_layout.addWidget(self.timer_label) self.layout().addLayout(button_layout) self.countdown = 8 self.timer = QTimer(self) self.timer.timeout.connect(self.update_countdown) self.timer.start(1000) # Mise à jour toutes les secondes def update_countdown(self): self.countdown -= 1 if self.countdown > 0: self.timer_label.setText(f"Suppression automatique dans {self.countdown} secondes...") else: self.timer.stop() self.accept() def reject(self): self.timer.stop() super().reject() def accept(self): self.timer.stop() super().accept() stop_trace() def main(): if sys.platform == 'win32': if sys.getwindowsversion().major >= 6: ctypes.windll.user32.SetProcessDPIAware() app_id = 'votre_entreprise.mailer_auto_click.version' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) try: app = QApplication(sys.argv) logger.info("Démarrage de l'application") main_window = QMainWindow() main_window.font_size = 10 # Créer et configurer l'interface principale main_interface = QWidget() main_layout = QVBoxLayout(main_interface) main_window.setCentralWidget(main_interface) # Associer l'interface au main_window pour un accès futur main_window.main_interface = main_interface main_window.main_interface_layout = main_layout # Méthode pour afficher un message d'erreur def show_error_message(message): QMessageBox.critical(main_window, "Erreur", message) main_window.show_error_message = show_error_message # Appeler ta fonction de configuration réelle setup_interface(main_window) main_window.show() logger.info("Fenêtre principale affichée") return app.exec_() except Exception as e: logger.error(f"Une erreur critique s'est produite : {e}", exc_info=True) write_debug_file() QMessageBox.critical(None, "Erreur critique", f"Une erreur inattendue s'est produite : {e}\n\nL'application va se fermer.") sys.exit(1) if __name__ == "__main__": main()