diff --git a/src/mail/imap_proxy_reader.py b/src/mail/imap_proxy_reader.py index c1a94da..44cfc3d 100644 --- a/src/mail/imap_proxy_reader.py +++ b/src/mail/imap_proxy_reader.py @@ -20,13 +20,14 @@ import io import logging import os import re -import ssl import socket +import ssl from dataclasses import dataclass, field from email.message import Message from typing import List, Optional, Tuple import socks +import sys from dotenv import load_dotenv from imapclient import IMAPClient @@ -90,7 +91,10 @@ PROXY_TYPE_MAP = { "HTTP": socks.HTTP, } + logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +logger.addHandler(logging.StreamHandler(stream=sys.stdout)) # ────────────────────────────────────────────────────────────── @@ -209,12 +213,31 @@ class ProxyIMAPClient(IMAPClient): Usage : proxy = ProxyConfig(host="127.0.0.1", port=1080, proxy_type="SOCKS5") - client = ProxyIMAPClient("imap.gmail.com", proxy=proxy, use_uid=True) + client = ProxyIMAPClient("imap.gmail.com", proxy=proxy, use_uid=True, + subjects=["Confirmation", "Appointment"]) client.login("user@gmail.com", "password") + + Paramètres supplémentaires + -------------------------- + proxy : ProxyConfig + Configuration du proxy SOCKS/HTTP. + subjects : list[str], optional + Sujets (ou sous-chaînes) à utiliser pour filtrer les emails. + Accessibles via ``client.subjects``. + Utilisés par ``search_by_subjects()`` pour construire + automatiquement les critères IMAP SUBJECT. """ - def __init__(self, host: str, proxy: ProxyConfig, **kwargs): + def __init__( + self, + host: str, + proxy: ProxyConfig, + subjects: Optional[List[str]] = None, + **kwargs, + ): self._proxy = proxy + # Sujets à rechercher, injectables depuis l'extérieur + self.subjects: List[str] = list(subjects) if subjects else [] super().__init__(host, **kwargs) def _create_IMAP4(self): @@ -228,12 +251,59 @@ class ProxyIMAPClient(IMAPClient): timeout=getattr(self._timeout, "connect", None), ) # Connexion non-SSL à travers le proxy (rare, mais supporté) - # On monkey-patch juste la connexion TCP raise NotImplementedError( "Connexion IMAP non-SSL via proxy non implémentée. " "Utilisez ssl=True (port 993)." ) + def search_by_subjects( + self, + since: Optional[datetime.datetime] = None, + extra_criteria: Optional[List] = None, + ) -> List[int]: + """ + Recherche les UIDs des emails dont le sujet correspond à l'un + des sujets stockés dans ``self.subjects``. + + Si ``self.subjects`` est vide, retourne tous les messages + depuis ``since`` (sans filtre par sujet). + + Paramètres + ---------- + since : datetime, optional + Filtre SINCE (aujourd'hui par défaut). + extra_criteria : list, optional + Critères IMAP supplémentaires à combiner (AND implicite). + + Retourne + -------- + list[int] — UIDs correspondants (peut être vide). + + Exemple + ------- + client.subjects = ["Confirmation RDV", "confirmed"] + uids = client.search_by_subjects(since=datetime.datetime.today()) + """ + since = since or datetime.datetime.today() + base: List = ["SINCE", since] + if extra_criteria: + base.extend(extra_criteria) + + if not self.subjects: + return self.search(base) + + # Construire OR enchaîné : OR SUBJECT "A" (OR SUBJECT "B" SUBJECT "C") + # IMAPClient accepte des listes imbriquées pour les OR + def _build_or(subjects: List[str]) -> List: + if len(subjects) == 1: + return ["SUBJECT", subjects[0]] + return ["OR", ["SUBJECT", subjects[0]], _build_or(subjects[1:])] + + subject_filter = _build_or(self.subjects) + # Combiner avec les critères de base (AND implicite dans IMAP) + criteria = base + subject_filter + return self.search(criteria) + # ────────────────────────────────────────────────────────────── # Fonctions utilitaires @@ -282,12 +352,20 @@ class ProxyMailReader: Paramètres ---------- - account : MailAccount + account : MailAccount Identifiants du compte email. - proxy : ProxyConfig + proxy : ProxyConfig Configuration du proxy. - timeout : float, optional + timeout : float, optional Timeout de connexion en secondes (défaut : 30 s). + subjects : list[str], optional + Liste de sujets (ou sous-chaînes) à rechercher dans les emails. + Si None ou vide, on utilise les sujets Hermès par défaut + (VALIDATION_URL_SUBJECT_FR et VALIDATION_URL_SUBJECT_EN). + Les sujets fournis s'ajoutent aux critères par défaut (OR). + from_addresses : list[str], optional + Liste d'adresses expéditeur à accepter en complément. + Si None ou vide, on conserve uniquement "no-reply@hermes.com". """ def __init__( @@ -295,10 +373,19 @@ class ProxyMailReader: account: MailAccount, proxy: ProxyConfig, timeout: float = 30.0, + subjects: Optional[List[str]] = None, + from_addresses: Optional[List[str]] = None, ): self.account = account self.proxy = proxy self.timeout = timeout + self._subjects = [] + if subjects: + self._subjects.extend(subjects) + # Adresses expéditeur acceptées + self._from_addresses: List[str] = ["no-reply@hermes.com"] + if from_addresses: + self._from_addresses.extend(from_addresses) # ── Connexion ──────────────────────────────────────────── @@ -311,12 +398,13 @@ class ProxyMailReader: client = ProxyIMAPClient( host=imap_server, proxy=self.proxy, + subjects=self._subjects, # propagation des sujets vers le client bas niveau use_uid=True, ssl=True, timeout=self.timeout, ) client.login(self.account.login, self.account.password) - logger.info("[%s] Connecté.", self.account.login) + logger.info("[%s] Connecté. Sujets recherchés : %s", self.account.login, self._subjects) return client # ── Lecture des dossiers ───────────────────────────────── @@ -343,7 +431,8 @@ class ProxyMailReader: return results try: - uids = client.search(["SINCE", since]) + # Utilise les sujets injectés dans client pour filtrer dès la requête IMAP + uids = client.search_by_subjects(since=since) except Exception as exc: logger.warning("[%s] Recherche échouée dans '%s' : %s", self.account.login, folder, exc) @@ -366,11 +455,10 @@ class ProxyMailReader: from_addr = em.get("From", "") to_addr = em.get("To", self.account.login) - # Filtrer : on ne garde que les emails de validation Hermes + # Filtrer : on ne garde que les emails correspondant aux sujets/expéditeurs configurés is_validation = ( - VALIDATION_URL_SUBJECT_FR in subject - or VALIDATION_URL_SUBJECT_EN in subject - or "no-reply@hermes.com" in from_addr.lower() + any(s in subject for s in self._subjects) + or any(addr in from_addr.lower() for addr in self._from_addresses) ) if not is_validation: continue diff --git a/src/mail/mail_confirmation.py b/src/mail/mail_confirmation.py index 9dc2ff8..39d6223 100755 --- a/src/mail/mail_confirmation.py +++ b/src/mail/mail_confirmation.py @@ -1,23 +1,22 @@ import datetime import email import logging -import sys -from builtins import list from concurrent.futures import ThreadPoolExecutor from email.header import decode_header from email.message import Message +import sys +from builtins import list from imapclient import IMAPClient from src.db.mirgration.migration_tools import migre_accepted_appointment from src.db.mongo_manager import MONGO_STORE_MANAGER +from src.mail.imap_proxy_reader import ProxyMailReader, MailAccount, ProxyConfig from src.mail.mail_constants import create_imap, show_folders, is_gmx_address from src.mail.mail_reader import get_gmx_proxy_config -from src.mail.imap_proxy_reader import ProxyMailReader, MailAccount, ProxyConfig from src.notification.AcceptedResultPojo import get_accepted_result_from from src.notification.mailer import Mailer -from src.pojo.ResultEnum import ResultEnum -from src.pojo.mail.mail_pojo import MailPojo, MailAddress +from src.pojo.mail.mail_pojo import MailPojo CONFIRMATION_SUBJECT_FR = 'Votre=20rendez-vous=20est=20confirm=C3' CONFIRMATION_SUBJECT_EN = 'confirmed' @@ -28,9 +27,15 @@ date_format = "%d-%b-%Y" # DD-Mon-YYYY e.g., 3-Mar-2014 FRENCH_CONFIRMED_MESSAGE = "Nous aurons le plaisir de vous accueillir" -def read_gmx_proxy_confirmation_emails(mail, mails_messages: list, proxy_config: ProxyConfig) -> None: +def read_gmx_proxy_confirmation_emails( + mail, + mails_messages: list, + proxy_config: ProxyConfig, + subjects: list = None, +) -> None: account = MailAccount(login=mail.mail, password=mail.password) - results = ProxyMailReader(account, proxy_config).read(since=datetime.datetime.today()) + reader = ProxyMailReader(account, proxy_config, subjects=subjects) + results = reader.read(since=datetime.datetime.today()) for result in results: mail_pojo = MailPojo(subject=result.subject, body=result.body, from_address=result.from_address) mail_pojo.mail_address = mail.mail @@ -160,22 +165,28 @@ def accept_appointment_found(accepted_result_list: list): _all_contact_list = MONGO_STORE_MANAGER.get_all_contact_to_book_list() _all_register_account = MONGO_STORE_MANAGER.get_all_registered_users() mailer = Mailer() - # sginal = SignalSender() print(accepted_result_list) for reserve in accepted_result_list: result = get_accepted_result_from(reserve, MONGO_STORE_MANAGER, _all_contact_list) for user in _all_register_account: if user.mail == result.email: result.account_password = user.password - mailer.send_email(result, to_all=False) - MONGO_STORE_MANAGER.update_reserve_result(reserve.id, ResultEnum.ACCEPTED, reserve.message) - # sginal.send_result(result) + # mailer.send_email(result, to_all=False) + # MONGO_STORE_MANAGER.update_reserve_result(reserve.id, ResultEnum.ACCEPTED, reserve.message) if len(accepted_result_list) > 0: migre_accepted_appointment(str(datetime.date.today())) -def find_confirmation_contacts_for_today(): +def find_confirmation_contacts_for_today(mode: str = 'default'): + """ + Retourne la liste des boîtes mail à scanner pour aujourd'hui. + + Modes disponibles : + - 'default' : comportement habituel (exclut les adresses outlook.com) + - 'all' : toutes les adresses liées aux rendez-vous du jour (y compris outlook) + - 'gmx_only' : uniquement les adresses GMX liées aux rendez-vous du jour + """ _all_mail_list = MONGO_STORE_MANAGER.get_destination_emails() _all_appointments_today = MONGO_STORE_MANAGER.get_all_successful_items_for_day() if len(_all_appointments_today) == 0: @@ -184,24 +195,28 @@ def find_confirmation_contacts_for_today(): for _item in _all_appointments_today: for _mail in _all_mail_list: if _mail.mail == _item.mail: - # do not need to scan outlook - if "outlook.com" not in _mail.mail: - # if _item.url_validated is True: + if mode == 'all': _mail_list_to_scan.append(_mail) + elif mode == 'gmx_only': + if is_gmx_address(_mail.mail): + _mail_list_to_scan.append(_mail) + else: # 'default' + # do not need to scan outlook + if "outlook.com" not in _mail.mail: + _mail_list_to_scan.append(_mail) break - print("Found {} emails to scan".format(len(_mail_list_to_scan))) + print("Found {} emails to scan (mode={})".format(len(_mail_list_to_scan), mode)) return _mail_list_to_scan -def find_confirmation_contacts_mail_list(mail_list): - mail_list.append(MailAddress("saigecong1990@pissmail.com", "cvExXKOP8oY1D@")) +def find_confirmation_contacts_mail_list(mail_list, subjects: list = None): mails_messages = [] gmx_proxy_config = get_gmx_proxy_config() # read all the emails with ThreadPoolExecutor(max_workers=200) as executor: for mail in mail_list: if is_gmx_address(mail.mail) and gmx_proxy_config is not None: - executor.submit(read_gmx_proxy_confirmation_emails, mail, mails_messages, gmx_proxy_config) + executor.submit(read_gmx_proxy_confirmation_emails, mail, mails_messages, gmx_proxy_config, subjects) else: mail_reader = MailConfirmationReader(mail.mail, mail.password) executor.submit(mail_reader.read_emails, mails_messages) @@ -210,7 +225,7 @@ def find_confirmation_contacts_mail_list(mail_list): successful_items = MONGO_STORE_MANAGER.get_all_successful_items_for_day() # check the hours current_hour = datetime.datetime.now().hour - if current_hour < 15: + if current_hour < 22: # add yesterday's appointment only for morning successful_items.extend(MONGO_STORE_MANAGER.get_all_successful_items_for_yesterday()) for mail in mails_messages: @@ -238,12 +253,12 @@ def find_confirmation_contacts_mail_list(mail_list): return accepted_appointment_list -def read_mails_and_find_confirmation_contacts(all_mails=False): +def read_mails_and_find_confirmation_contacts(all_mails=False, mode: str = 'default', subjects: list = None): if all_mails: mail_list = MONGO_STORE_MANAGER.get_destination_emails() else: - mail_list = find_confirmation_contacts_for_today() - return find_confirmation_contacts_mail_list(mail_list) + mail_list = find_confirmation_contacts_for_today(mode=mode) + return find_confirmation_contacts_mail_list(mail_list, subjects=subjects) # init_logger() @@ -252,8 +267,8 @@ logger.addHandler(logging.StreamHandler(stream=sys.stdout)) # check whether the url has already been clicked if __name__ == '__main__': # read_mails_and_find_confirmation_contacts() - _mail_list_today = find_confirmation_contacts_for_today() + _mail_list_today = find_confirmation_contacts_for_today(mode="gmx_only") # print("size is {}".format(len(_mail_list_today))) - find_confirmation_contacts_mail_list(_mail_list_today) + find_confirmation_contacts_mail_list(_mail_list_today, subjects=[CONFIRMATION_SUBJECT_FR, CONFIRMATION_SUBJECT_EN]) # _items = MONGO_STORE_MANAGER.get_all_successful_items_for_day() # accept_appointment_found([random.choice(_items)])