need to test read confirmation mails

This commit is contained in:
2026-03-29 19:13:20 +02:00
parent 9802848c5f
commit e8b0a4aae9
2 changed files with 141 additions and 38 deletions
+101 -13
View File
@@ -20,13 +20,14 @@ import io
import logging import logging
import os import os
import re import re
import ssl
import socket import socket
import ssl
from dataclasses import dataclass, field from dataclasses import dataclass, field
from email.message import Message from email.message import Message
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
import socks import socks
import sys
from dotenv import load_dotenv from dotenv import load_dotenv
from imapclient import IMAPClient from imapclient import IMAPClient
@@ -90,7 +91,10 @@ PROXY_TYPE_MAP = {
"HTTP": socks.HTTP, "HTTP": socks.HTTP,
} }
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(stream=sys.stdout))
# ────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────
@@ -209,12 +213,31 @@ class ProxyIMAPClient(IMAPClient):
Usage : Usage :
proxy = ProxyConfig(host="127.0.0.1", port=1080, proxy_type="SOCKS5") 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") 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 self._proxy = proxy
# Sujets à rechercher, injectables depuis l'extérieur
self.subjects: List[str] = list(subjects) if subjects else []
super().__init__(host, **kwargs) super().__init__(host, **kwargs)
def _create_IMAP4(self): def _create_IMAP4(self):
@@ -228,12 +251,59 @@ class ProxyIMAPClient(IMAPClient):
timeout=getattr(self._timeout, "connect", None), timeout=getattr(self._timeout, "connect", None),
) )
# Connexion non-SSL à travers le proxy (rare, mais supporté) # Connexion non-SSL à travers le proxy (rare, mais supporté)
# On monkey-patch juste la connexion TCP
raise NotImplementedError( raise NotImplementedError(
"Connexion IMAP non-SSL via proxy non implémentée. " "Connexion IMAP non-SSL via proxy non implémentée. "
"Utilisez ssl=True (port 993)." "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 # Fonctions utilitaires
@@ -282,12 +352,20 @@ class ProxyMailReader:
Paramètres Paramètres
---------- ----------
account : MailAccount account : MailAccount
Identifiants du compte email. Identifiants du compte email.
proxy : ProxyConfig proxy : ProxyConfig
Configuration du proxy. Configuration du proxy.
timeout : float, optional timeout : float, optional
Timeout de connexion en secondes (défaut : 30 s). 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__( def __init__(
@@ -295,10 +373,19 @@ class ProxyMailReader:
account: MailAccount, account: MailAccount,
proxy: ProxyConfig, proxy: ProxyConfig,
timeout: float = 30.0, timeout: float = 30.0,
subjects: Optional[List[str]] = None,
from_addresses: Optional[List[str]] = None,
): ):
self.account = account self.account = account
self.proxy = proxy self.proxy = proxy
self.timeout = timeout 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 ──────────────────────────────────────────── # ── Connexion ────────────────────────────────────────────
@@ -311,12 +398,13 @@ class ProxyMailReader:
client = ProxyIMAPClient( client = ProxyIMAPClient(
host=imap_server, host=imap_server,
proxy=self.proxy, proxy=self.proxy,
subjects=self._subjects, # propagation des sujets vers le client bas niveau
use_uid=True, use_uid=True,
ssl=True, ssl=True,
timeout=self.timeout, timeout=self.timeout,
) )
client.login(self.account.login, self.account.password) 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 return client
# ── Lecture des dossiers ───────────────────────────────── # ── Lecture des dossiers ─────────────────────────────────
@@ -343,7 +431,8 @@ class ProxyMailReader:
return results return results
try: 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: except Exception as exc:
logger.warning("[%s] Recherche échouée dans '%s' : %s", logger.warning("[%s] Recherche échouée dans '%s' : %s",
self.account.login, folder, exc) self.account.login, folder, exc)
@@ -366,11 +455,10 @@ class ProxyMailReader:
from_addr = em.get("From", "") from_addr = em.get("From", "")
to_addr = em.get("To", self.account.login) 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 = ( is_validation = (
VALIDATION_URL_SUBJECT_FR in subject any(s in subject for s in self._subjects)
or VALIDATION_URL_SUBJECT_EN in subject or any(addr in from_addr.lower() for addr in self._from_addresses)
or "no-reply@hermes.com" in from_addr.lower()
) )
if not is_validation: if not is_validation:
continue continue
+40 -25
View File
@@ -1,23 +1,22 @@
import datetime import datetime
import email import email
import logging import logging
import sys
from builtins import list
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from email.header import decode_header from email.header import decode_header
from email.message import Message from email.message import Message
import sys
from builtins import list
from imapclient import IMAPClient from imapclient import IMAPClient
from src.db.mirgration.migration_tools import migre_accepted_appointment from src.db.mirgration.migration_tools import migre_accepted_appointment
from src.db.mongo_manager import MONGO_STORE_MANAGER 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_constants import create_imap, show_folders, is_gmx_address
from src.mail.mail_reader import get_gmx_proxy_config 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.AcceptedResultPojo import get_accepted_result_from
from src.notification.mailer import Mailer from src.notification.mailer import Mailer
from src.pojo.ResultEnum import ResultEnum from src.pojo.mail.mail_pojo import MailPojo
from src.pojo.mail.mail_pojo import MailPojo, MailAddress
CONFIRMATION_SUBJECT_FR = 'Votre=20rendez-vous=20est=20confirm=C3' CONFIRMATION_SUBJECT_FR = 'Votre=20rendez-vous=20est=20confirm=C3'
CONFIRMATION_SUBJECT_EN = 'confirmed' 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" 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) 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: for result in results:
mail_pojo = MailPojo(subject=result.subject, body=result.body, from_address=result.from_address) mail_pojo = MailPojo(subject=result.subject, body=result.body, from_address=result.from_address)
mail_pojo.mail_address = mail.mail 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_contact_list = MONGO_STORE_MANAGER.get_all_contact_to_book_list()
_all_register_account = MONGO_STORE_MANAGER.get_all_registered_users() _all_register_account = MONGO_STORE_MANAGER.get_all_registered_users()
mailer = Mailer() mailer = Mailer()
# sginal = SignalSender()
print(accepted_result_list) print(accepted_result_list)
for reserve in accepted_result_list: for reserve in accepted_result_list:
result = get_accepted_result_from(reserve, MONGO_STORE_MANAGER, _all_contact_list) result = get_accepted_result_from(reserve, MONGO_STORE_MANAGER, _all_contact_list)
for user in _all_register_account: for user in _all_register_account:
if user.mail == result.email: if user.mail == result.email:
result.account_password = user.password result.account_password = user.password
mailer.send_email(result, to_all=False) # mailer.send_email(result, to_all=False)
MONGO_STORE_MANAGER.update_reserve_result(reserve.id, ResultEnum.ACCEPTED, reserve.message) # MONGO_STORE_MANAGER.update_reserve_result(reserve.id, ResultEnum.ACCEPTED, reserve.message)
# sginal.send_result(result)
if len(accepted_result_list) > 0: if len(accepted_result_list) > 0:
migre_accepted_appointment(str(datetime.date.today())) 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_mail_list = MONGO_STORE_MANAGER.get_destination_emails()
_all_appointments_today = MONGO_STORE_MANAGER.get_all_successful_items_for_day() _all_appointments_today = MONGO_STORE_MANAGER.get_all_successful_items_for_day()
if len(_all_appointments_today) == 0: if len(_all_appointments_today) == 0:
@@ -184,24 +195,28 @@ def find_confirmation_contacts_for_today():
for _item in _all_appointments_today: for _item in _all_appointments_today:
for _mail in _all_mail_list: for _mail in _all_mail_list:
if _mail.mail == _item.mail: if _mail.mail == _item.mail:
# do not need to scan outlook if mode == 'all':
if "outlook.com" not in _mail.mail:
# if _item.url_validated is True:
_mail_list_to_scan.append(_mail) _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 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 return _mail_list_to_scan
def find_confirmation_contacts_mail_list(mail_list): def find_confirmation_contacts_mail_list(mail_list, subjects: list = None):
mail_list.append(MailAddress("saigecong1990@pissmail.com", "cvExXKOP8oY1D@"))
mails_messages = [] mails_messages = []
gmx_proxy_config = get_gmx_proxy_config() gmx_proxy_config = get_gmx_proxy_config()
# read all the emails # read all the emails
with ThreadPoolExecutor(max_workers=200) as executor: with ThreadPoolExecutor(max_workers=200) as executor:
for mail in mail_list: for mail in mail_list:
if is_gmx_address(mail.mail) and gmx_proxy_config is not None: 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: else:
mail_reader = MailConfirmationReader(mail.mail, mail.password) mail_reader = MailConfirmationReader(mail.mail, mail.password)
executor.submit(mail_reader.read_emails, mails_messages) 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() successful_items = MONGO_STORE_MANAGER.get_all_successful_items_for_day()
# check the hours # check the hours
current_hour = datetime.datetime.now().hour current_hour = datetime.datetime.now().hour
if current_hour < 15: if current_hour < 22:
# add yesterday's appointment only for morning # add yesterday's appointment only for morning
successful_items.extend(MONGO_STORE_MANAGER.get_all_successful_items_for_yesterday()) successful_items.extend(MONGO_STORE_MANAGER.get_all_successful_items_for_yesterday())
for mail in mails_messages: for mail in mails_messages:
@@ -238,12 +253,12 @@ def find_confirmation_contacts_mail_list(mail_list):
return accepted_appointment_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: if all_mails:
mail_list = MONGO_STORE_MANAGER.get_destination_emails() mail_list = MONGO_STORE_MANAGER.get_destination_emails()
else: else:
mail_list = find_confirmation_contacts_for_today() mail_list = find_confirmation_contacts_for_today(mode=mode)
return find_confirmation_contacts_mail_list(mail_list) return find_confirmation_contacts_mail_list(mail_list, subjects=subjects)
# init_logger() # init_logger()
@@ -252,8 +267,8 @@ logger.addHandler(logging.StreamHandler(stream=sys.stdout))
# check whether the url has already been clicked # check whether the url has already been clicked
if __name__ == '__main__': if __name__ == '__main__':
# read_mails_and_find_confirmation_contacts() # 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))) # 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() # _items = MONGO_STORE_MANAGER.get_all_successful_items_for_day()
# accept_appointment_found([random.choice(_items)]) # accept_appointment_found([random.choice(_items)])