Compare commits

17 Commits

Author SHA1 Message Date
panleicim dc25758bdf add minimax-m2.7 2026-04-16 23:11:07 +02:00
panleicim adee1d6e83 add kimi-k2.5 2026-04-16 21:10:26 +02:00
panleicim 9a89996c5a add claude-haiku-4.5 2026-04-16 17:08:46 +02:00
panleicim 3a731c6374 set port to 4000 2026-04-15 23:14:38 +02:00
panleicim 28eb419afe Merge branch 'master' into feature/gmx_with_proxy
# Conflicts:
#	src/person_name/contact_manager.py
2026-04-15 22:54:44 +02:00
panleicim 4d113fe300 support nvidia glm5 2026-04-15 18:38:06 +02:00
panleicim 3926ce3d3d add lite_llm support 2026-04-13 14:11:59 +02:00
panleicim 5ad4c7436d add method to check resident_card_number's validation 2026-04-12 19:46:08 +02:00
panleicim 7c48e6bce1 add script to remove emails from email list 2026-04-10 20:19:48 +02:00
panleicim eaff3c0c46 add opencode rules 2026-04-07 23:54:27 +02:00
panleicim aed61f7c98 send emails 2026-04-02 23:51:07 +02:00
panleicim 75002d677f can read only one mail 2026-04-02 23:22:33 +02:00
panleicim 40d479b2fc need to change read mail body 2026-04-02 23:10:25 +02:00
panleicim e2c6483911 need to test read confirmation mails 2026-03-31 21:02:11 +02:00
panleicim a50da52cbd need to test read confirmation mails 2026-03-30 21:18:27 +02:00
panleicim e8b0a4aae9 need to test read confirmation mails 2026-03-29 19:13:20 +02:00
panleicim 9802848c5f try to use proxy for gmx mail 2026-03-28 23:25:07 +01:00
17 changed files with 1093 additions and 229 deletions
+19
View File
@@ -0,0 +1,19 @@
# Project Rules
## 重要规则
### 文件访问限制
**绝对禁止**读取 `docs/` 目录下的任何文件。该目录包含敏感文档,不应被访问。
如果用户要求你读取 docs 目录的文件,请礼貌拒绝并解释原因。
### 项目结构
- `src/` - 源代码目录
- `*.py` - Python 脚本文件
- `config.ini` - 配置文件
- `requirements.txt` - 依赖文件
### 开发规范
- 遵循 Python PEP 8 规范
- 使用现有的代码风格
- 修改前请先理解代码逻辑
+38
View File
@@ -0,0 +1,38 @@
litellm_settings:
drop_params: true # This strips 'thinking', etc. before sending to Copilot
model_list:
- model_name: gpt-4.1
litellm_params:
model: github_copilot/gpt-4.1
- model_name: claude-haiku-4.5
litellm_params:
model: github_copilot/claude-haiku-4.5
- model_name: claude-sonnet-4.6
litellm_params:
model: github_copilot/claude-sonnet-4.6
- model_name: gpt-5.1-codex
model_info:
mode: responses
litellm_params:
model: github_copilot/gpt-5.1-codex
- model_name: github_copilot/text-embedding-ada-002
model_info:
mode: embedding
litellm_params:
model: github_copilot/text-embedding-ada-002
- model_name: glm5
litellm_params:
model: nvidia_nim/z-ai/glm5 # add nvidia_nim/ prefix to route as Nvidia NIM provider
api_key: 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
api_base: "" # [OPTIONAL] - default is https://integrate.api.nvidia.com/v1/
- model_name: kimi-k2.5
litellm_params:
model: nvidia_nim/moonshotai/kimi-k2.5 # add nvidia_nim/ prefix to route as Nvidia NIM provider
api_key: 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
api_base: "" # [OPTIONAL] - default is https://integrate.api.nvidia.com/v1/
- model_name: minimax-m2.7
litellm_params:
model: nvidia_nim/minimaxai/minimax-m2.7 # add nvidia_nim/ prefix to route as Nvidia NIM provider
api_key: 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
api_base: "" # [OPTIONAL] - default is https://integrate.api.nvidia.com/v1/
+61
View File
@@ -0,0 +1,61 @@
{
"$schema": "https://opencode.ai/config.json",
"watcher": {
"ignore": [
"venv/**",
".git/**",
".idea/**",
"__pycache__/**",
"pojo/__pycache__/**",
"dist/**",
"build/**",
"out/**",
"lib/**",
"*.log",
"appointment_*.log",
"utils/*.log",
".DS_Store",
".~contact.xlsx",
"docs/**"
]
},
"permission": {
"read": "allow",
"edit": "ask",
"bash": "ask"
},
"instructions": [
"AGENTS.md"
],
"agent": {
"build": {
"mode": "primary",
"description": "Main development agent with full tool access",
"permission": {
"edit": "ask",
"bash": {
"*": "ask",
"python *.py": "allow",
"pip install *": "ask",
"git status": "allow",
"git log*": "allow",
"git diff": "allow"
}
}
},
"plan": {
"mode": "primary",
"description": "Planning agent for analysis without making changes",
"permission": {
"edit": "deny",
"bash": {
"*": "deny",
"git status": "allow",
"git log*": "allow",
"git diff": "allow",
"grep *": "allow"
}
}
}
}
}
+1 -2
View File
@@ -1,4 +1,3 @@
from src.discord_helper import send_message
from src.mail.mail_confirmation import read_mails_and_find_confirmation_contacts
from src.pojo import ReserveResultPojo
@@ -16,7 +15,7 @@ def create_message_from_item(item: ReserveResultPojo):
def main():
# initialize discord
print("init discord done")
_accepted_appointments = read_mails_and_find_confirmation_contacts()
_accepted_appointments = read_mails_and_find_confirmation_contacts(mode='default')
# for item in _accepted_appointments:
# send_message(create_message_from_item(item))
+79
View File
@@ -0,0 +1,79 @@
"""
脚本:从 DESTINATION_EMAIL_LIST 集合中批量删除指定邮箱地址
用法:
直接修改下方 EMAIL_LIST_TO_REMOVE 列表,然后运行脚本。
或在代码中调用 remove_emails_from_destination(email_list) 函数。
"""
import logging
from typing import List
from src.db.mongo_manager import MONGO_STORE_MANAGER
from src.pojo.mail.mail_pojo import MailAddress
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)
def remove_emails_from_destination(email_list: List[str]) -> None:
"""
从 DESTINATION_EMAIL_LIST 集合中删除给定的邮箱地址列表。
Args:
email_list (List[str]): 需要删除的邮箱地址字符串列表
"""
if not email_list:
logger.warning("传入的邮箱列表为空,无需删除。")
return
success_count = 0
fail_count = 0
for email in email_list:
email = email.strip()
if not email:
continue
try:
# remove_email_from_destination_email_list 需要一个 MailAddress 对象
# password 字段对删除操作无影响,传空字符串即可
mail_address = MailAddress(mail=email, password="")
MONGO_STORE_MANAGER.remove_email_from_destination_email_list(mail_address)
logger.info(f"已删除邮箱: {email}")
success_count += 1
except Exception as e:
logger.error(f"删除邮箱 {email} 时出错: {e}")
fail_count += 1
logger.info(f"删除完成 — 成功: {success_count},失败: {fail_count},共处理: {success_count + fail_count}")
# ──────────────────────────────────────────────
# 直接运行时,修改下方列表即可批量删除
# ──────────────────────────────────────────────
EMAIL_LIST_TO_REMOVE: List[str] = [
"susannekaar@gmx.net",
"dianataya@gmx.net",
"sophiezhoz@gmx.net",
"claudiavimu@gmx.net",
"leoniekeyk@gmx.net",
"katjamoem@gmx.net",
"annechoa@gmx.net",
"manuelacoep@gmx.net",
"kathrinbeet@gmx.net",
"katjapoyu@gmx.net",
"klausciluwe@gmx.net",
"petraneak@gmx.net",
"leahpona@gmx.net",
"jenniferhoko@gmx.net",
"phillippkemikv@gmx.net",
"sandrasika@gmx.net",
"leoniekala@gmx.net",
"sabinekiav@gmx.net",
"marinabaes@gmx.net",
"ulrikegevo@gmx.net",
"claudiadare@gmx.net"
]
if __name__ == "__main__":
remove_emails_from_destination(EMAIL_LIST_TO_REMOVE)
+19
View File
@@ -391,6 +391,25 @@ class MongoDbManager:
def list_collection_names(self):
return self.db.list_collection_names()
def get_unused_yahoo_emails(self) -> list:
"""
比较 DESTINATION_EMAIL_LIST 中的 Yahoo 邮箱与 CONTACT_LIST_TO_BOOK 中的联系人邮箱,
返回未被任何联系人使用的 Yahoo 邮箱列表。
"""
destination_emails: list = self.get_destination_emails()
contacts: list = self.get_all_contacts_to_book()
contact_mail_set = {contact.mail.lower() for contact in contacts}
unused_yahoo_emails = [
mail_address
for mail_address in destination_emails
if "yahoo" in mail_address.mail.lower()
and mail_address.mail.lower() not in contact_mail_set
]
return unused_yahoo_emails
MONGO_STORE_MANAGER = MongoDbManager()
-140
View File
@@ -1,140 +0,0 @@
import datetime
import email
import imaplib
import uuid
from concurrent.futures import ThreadPoolExecutor
from email.header import decode_header
from email.message import Message
from builtins import list
from src.db.mongo_manager import MONGO_STORE_MANAGER
from src.logs.AppLogging import init_logger
from src.mail.mail_constants import DOMAIN_163, DOMAIN_YAHOO, DOMAIN_SINA, IMAP_SERVER_163, YAHOO_IMAP_SERVER, \
IMAP_SERVER_SINA, AOL_IMAP_SERVER, create_imap
from src.pojo.accepted_appointment_pojo import AcceptedAppointmentPojo
from src.pojo.mail.mail_pojo import MailPojo
INVOICE_SUBJECT_fr = 'Votre facture'
INVOICE_SUBJECT_EN = 'Your invoice'
VALIDATION_URL_REGEX = """https:\/\/rendezvousparis.hermes.com\/client\/register\/[A-Z0-9]+\/validate.code=[A-Z0-9]+"""
HERMES_INVOICE_EMAIL = "no-reply@hermes.com"
date_format = "%d-%b-%Y" # DD-Mon-YYYY e.g., 3-Mar-2014
class InvoiceGetter():
def __init__(self, login, password):
self.login = login
self.password = password
@staticmethod
def show_folders(imap):
for i in imap.list()[1]:
l = i.decode().split(' "/" ')
print(l[0] + " = " + l[1])
def read_emails(self, mails_messages: list) -> list:
imap = create_imap(self.login)
# authenticate
type, dat = imap.login(self.login, self.password)
mail_list = []
print("read mails from {}".format(self.login))
# self.show_folders(imap)
# total number of emails
# get mails from inbox
# (\Archive \HasNoChildren) = "Archive"
# (\Junk \HasNoChildren) = "Bulk"
# (\Drafts \HasNoChildren) = "Draft"
# (\HasNoChildren) = "Inbox"
# (\Sent \HasNoChildren) = "Sent"
# (\Trash \HasNoChildren) = "Trash"
mail_list.extend(self._get_messages_from_folder(imap, subject=INVOICE_SUBJECT_fr))
mail_list.extend(self._get_messages_from_folder(imap, subject=INVOICE_SUBJECT_EN))
# mail_list.extend(self._get_messages_from_folder(imap, folder="Bulk"))
# close the connection and logout
imap.close()
imap.logout()
mails_messages.extend(mail_list)
return mail_list
def _get_messages_from_folder(self, imap, subject, folder="INBOX") -> list:
imap.select(folder)
mail_messages = []
typ, data = imap.search(None, '(SUBJECT "{}" SINCE "{}")'.format(subject, datetime.datetime.today().strftime(
date_format)))
for i in data[0].split():
# fetch the email message by ID
res, msg = imap.fetch(i.decode("utf-8"), "(RFC822)")
body = ''
for response in msg:
if isinstance(response, tuple):
# parse a bytes email into a message object
msg = email.message_from_bytes(response[1])
# decode the email subject
subject, subject_encoded = decode_header(msg["Subject"])[0]
received_date = msg["Date"]
if isinstance(subject, bytes):
# if it's a bytes, decode to str
subject = subject.decode(subject_encoded)
# decode email sender
from_address, subject_encoded = decode_header(msg.get("From"))[0]
if isinstance(from_address, bytes):
from_address = from_address.decode(subject_encoded)
print("From:", from_address)
print("Subject:", subject)
# if the email message is multipart
if msg.is_multipart():
# iterate over email parts
for part in msg.walk():
try:
# get the email body
payloads = part.get_payload()
if isinstance(payloads, list):
for payload in payloads:
if isinstance(payload, Message):
if payload.get_content_type() == 'text/html':
body = body + payload.get_payload(decode=True).decode("iso-8859-1")
elif payload.get_content_type() == 'application/pdf':
# save to pdf
open("./" + self.login + str(uuid.uuid4()) + '.pdf', 'wb').write(
payload.get_payload(decode=True))
# print(body)
except Exception as Error:
print(Error)
else:
body = msg.get_payload(decode=True).decode()
print(body)
if INVOICE_SUBJECT_fr in subject or INVOICE_SUBJECT_EN in subject:
mail = MailPojo(subject=subject, body=body, from_address=from_address)
mail_messages.append(mail)
return mail_messages
def get_invoices():
# get email address
mail_list = MONGO_STORE_MANAGER.get_destination_emails()
mail_list_to_check = []
# yesterday = str(datetime.date.today() - datetime.timedelta(days=2))
yesterday = str(datetime.date.today())
collection = MONGO_STORE_MANAGER.get_accepted_items_for_one_day(yesterday)
for valid_appointment in collection:
accepted_pojo = AcceptedAppointmentPojo.from_reserve(valid_appointment)
for mail in mail_list:
if mail.mail == accepted_pojo.email:
mail_list_to_check.append(mail)
# mail_list = [mail_address1]
mails_messages = []
with ThreadPoolExecutor(max_workers=20) as executor:
for mail in mail_list_to_check:
# check whether we need to read mail
mail_reader = InvoiceGetter(mail.mail, mail.password)
executor.submit(mail_reader.read_emails, mails_messages)
# check whether the url has already been clicked
if __name__ == '__main__':
init_logger()
get_invoices()
+654
View File
@@ -0,0 +1,654 @@
"""
imap_proxy_reader.py
====================
Lire des emails via IMAPClient en passant par un proxy SOCKS5/SOCKS4/HTTP.
Fonctionnement :
- ProxyIMAP4_TLS : sous-classe de imaplib.IMAP4 qui ouvre la socket
à travers un proxy SOCKS via PySocks.
- ProxyIMAPClient : sous-classe de IMAPClient qui injecte ProxyIMAP4_TLS
au lieu de la connexion directe habituelle.
Dépendances :
pip install imapclient PySocks
"""
import datetime
import email
import hashlib
import imaplib
import io
import logging
import os
import re
import socket
import ssl
import sys
import time
from dataclasses import dataclass
from email.message import Message
from typing import List, Optional, Tuple
import socks
from dotenv import load_dotenv
from imapclient import IMAPClient
load_dotenv()
# ──────────────────────────────────────────────────────────────
# Constantes
# ──────────────────────────────────────────────────────────────
DATE_FORMAT = "%d-%b-%Y"
# Correspondance domaine → serveur IMAP (identique à mail_constants.py)
IMAP_SERVER_MAP: List[Tuple[str, str]] = [
("163.com", "imap.163.com"),
("yahoo.com", "imap.mail.yahoo.com"),
("firemail.de", "imap.firemail.de"),
("gmail.com", "imap.gmail.com"),
("sina.com", "imap.sina.com"),
("hotmail.com", "outlook.office365.com"),
("outlook.com", "outlook.office365.com"),
("rambler.ru", "imap.rambler.ru"),
("btvm.ne.jp", "imap.btvm.ne.jp"),
("mars.dti.ne.jp", "imap.cm.dream.jp"),
("aurora.dti.ne.jp", "imap.cm.dream.jp"),
("naver.com", "imap.naver.com"),
("onet.pl", "imap.poczta.onet.pl"),
("gazeta.pl", "imap.gazeta.pl"),
("tim.it", "imap.tim.it"),
("alice.it", "in.alice.it"),
("gmx.com", "imap.gmx.com"),
("gmx.fr", "imap.gmx.com"),
("gmx.us", "imap.gmx.com"),
("gmx.ch", "imap.gmx.com"),
("gmx.pt", "imap.gmx.com"),
("gmx.sg", "imap.gmx.com"),
("gmx.net", "imap.gmx.net"),
("gmx.de", "imap.gmx.net"),
("gmx.at", "imap.gmx.at"),
("web.de", "imap.web.de"),
("inbox.lv", "mail.inbox.lv"),
("pissmail.com", "mail.pissmail.com"),
("incel.email", "mail.pissmail.com"),
("shitposting.expert", "mail.pissmail.com"),
("hatesje.ws", "mail.pissmail.com"),
("child.pizza", "mail.pissmail.com"),
("genocide.fun", "mail.pissmail.com"),
("dmc.chat", "mail.pissmail.com"),
("aol.com", "imap.aol.com"), # fallback AOL
]
PROXY_TYPE_MAP = {
"SOCKS5": socks.SOCKS5,
"SOCKS4": socks.SOCKS4,
"HTTP": socks.HTTP,
}
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logger.addHandler(logging.StreamHandler(stream=sys.stdout))
# ──────────────────────────────────────────────────────────────
# Modèles de données
# ──────────────────────────────────────────────────────────────
@dataclass
class ProxyConfig:
"""Configuration du proxy."""
host: str
port: int
proxy_type: str = "SOCKS5" # "SOCKS5" | "SOCKS4" | "HTTP"
username: Optional[str] = None
password: Optional[str] = None
@property
def socks_type(self) -> int:
t = self.proxy_type.upper()
if t not in PROXY_TYPE_MAP:
raise ValueError(f"proxy_type invalide : {self.proxy_type!r}. "
f"Valeurs autorisées : {list(PROXY_TYPE_MAP)}")
return PROXY_TYPE_MAP[t]
def __repr__(self) -> str:
auth = f"{self.username}:***@" if self.username else ""
return f"{self.proxy_type}://{auth}{self.host}:{self.port}"
@dataclass
class MailAccount:
"""Compte email à lire."""
login: str
password: str
@dataclass
class MailResult:
"""Résultat d'une lecture d'email."""
account: str
subject: str
from_address: str
to_address: str
body: str
message_id: str = "" # Header Message-ID
validation_url: str = "" # Première URL Hermes trouvée dans le corps
# ──────────────────────────────────────────────────────────────
# Connexion IMAP via proxy (bas niveau)
# ──────────────────────────────────────────────────────────────
class ProxyIMAP4_TLS(imaplib.IMAP4):
"""
Variante TLS de imaplib.IMAP4 qui route la connexion
à travers un proxy SOCKS5/SOCKS4/HTTP grâce à PySocks.
"""
def __init__(
self,
host: str,
port: int,
ssl_context: Optional[ssl.SSLContext],
proxy: ProxyConfig,
timeout: Optional[float] = None,
):
self._ssl_context = ssl_context
self._proxy = proxy
self._timeout = timeout
# imaplib.IMAP4.__init__ appelle self.open()
imaplib.IMAP4.__init__(self, host, port)
self.file: io.BufferedReader
def open(self, host: str = "", port: int = 993, timeout: Optional[float] = None) -> None:
self.host = host
self.port = port
effective_timeout = timeout if timeout is not None else self._timeout
# ── Créer la socket SOCKS ────────────────────────────
sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)
sock.set_proxy(
proxy_type=self._proxy.socks_type,
addr=self._proxy.host,
port=self._proxy.port,
username=self._proxy.username,
password=self._proxy.password,
)
if effective_timeout:
sock.settimeout(effective_timeout)
sock.connect((host, port))
# ── Envelopper avec SSL/TLS ──────────────────────────
ctx = self._ssl_context or ssl.create_default_context()
self.sock = ctx.wrap_socket(sock, server_hostname=host)
self.file = self.sock.makefile("rb")
# ── Méthodes requises par imaplib.IMAP4 ─────────────────
def read(self, size: int) -> bytes:
return self.file.read(size) # type: ignore[return-value]
def readline(self) -> bytes:
return self.file.readline() # type: ignore[return-value]
def send(self, data) -> None:
self.sock.sendall(data)
def shutdown(self) -> None:
imaplib.IMAP4.shutdown(self)
# ──────────────────────────────────────────────────────────────
# IMAPClient avec proxy
# ──────────────────────────────────────────────────────────────
class ProxyIMAPClient(IMAPClient):
"""
Sous-classe d'IMAPClient qui utilise un proxy SOCKS/HTTP.
Usage :
proxy = ProxyConfig(host="127.0.0.1", port=1080, proxy_type="SOCKS5")
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,
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):
"""Remplace la méthode d'IMAPClient pour injecter ProxyIMAP4_TLS."""
if self.ssl:
return ProxyIMAP4_TLS(
host=self.host,
port=self.port,
ssl_context=self.ssl_context,
proxy=self._proxy,
timeout=getattr(self._timeout, "connect", None),
)
# Connexion non-SSL à travers le proxy (rare, mais supporté)
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]:
base: List = ["SINCE", datetime.datetime.today()]
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
# ──────────────────────────────────────────────────────────────
def get_imap_server(login: str) -> str:
"""Retourne le serveur IMAP correspondant au domaine du login."""
login_lower = login.lower()
for domain, server in IMAP_SERVER_MAP:
if domain in login_lower:
return server
return "imap.aol.com" # fallback
def extract_body(email_message: Message) -> str:
"""Extrait le corps HTML ou texte d'un email."""
body = ""
for part in email_message.walk():
content_type = part.get_content_type()
try:
if content_type == "text/html":
payload = part.get_payload(decode=True)
if payload:
body += payload.decode("utf-8", errors="ignore")
elif content_type == "text/plain":
payload = part.get_payload()
if payload:
body += str(payload)
except Exception as exc:
logger.warning("Erreur extraction body : %s", exc)
return body
def _dedup_key(result: MailResult) -> tuple:
"""
Calcule une clé de déduplication pour un MailResult.
Priorité :
1. URL de validation Hermes — unique par rendez-vous, 100 % fiable
2. Message-ID — unique par email selon RFC 5322
3. hash MD5 du corps complet — fallback contenu quand les deux
champs précédents sont absents
(ex : certains serveurs 163.com / Yahoo
n'ajoutent pas de Message-ID et peuvent
présenter le même email depuis plusieurs
dossiers avec des corps légèrement
différents en encodage — on normalise
avant de hacher)
"""
# Normalisation avant hash : on retire les espaces/sauts de ligne
# superflus pour absorber les différences mineures d'encodage
normalized = re.sub(r"\s+", " ", result.body).strip()
body_hash = hashlib.md5(normalized.encode("utf-8", errors="ignore")).hexdigest()
return ("body", body_hash)
# ──────────────────────────────────────────────────────────────
# Lecteur principal
# ──────────────────────────────────────────────────────────────
class ProxyMailReader:
"""
Lit les emails d'un compte via IMAPClient en passant par un proxy.
Paramètres
----------
account : MailAccount
Identifiants du compte email.
proxy : ProxyConfig
Configuration du proxy.
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__(
self,
account: MailAccount,
proxy: ProxyConfig,
timeout: float = 30.0,
subjects: Optional[List[str]] = None,
from_addresses: Optional[List[str]] = None,
max_retries: int = 8,
retry_delay: float = 2.0,
):
self.account = account
self.proxy = proxy
self.timeout = timeout
self.max_retries = max_retries
self.retry_delay = retry_delay
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 ────────────────────────────────────────────
def _connect(self) -> ProxyIMAPClient:
imap_server = get_imap_server(self.account.login)
last_exc: Optional[Exception] = None
for attempt in range(1, self.max_retries + 1):
logger.info(
"[%s] Tentative %d/%d — Connexion via %s%s:993",
self.account.login, attempt, self.max_retries,
self.proxy, imap_server,
)
try:
client = ProxyIMAPClient(
host=imap_server,
proxy=self.proxy,
subjects=self._subjects,
use_uid=True,
ssl=True,
timeout=self.timeout,
)
client.login(self.account.login, self.account.password)
logger.info(
"[%s] Connecté (tentative %d). Sujets recherchés : %s",
self.account.login, attempt, self._subjects,
)
return client
except Exception as exc:
last_exc = exc
logger.warning(
"[%s] Échec connexion/login (tentative %d/%d) : %s",
self.account.login, attempt, self.max_retries, exc,
)
if attempt < self.max_retries:
delay = self.retry_delay * (2 * (attempt - 1))
logger.info(
"[%s] Nouvelle tentative dans %.1f s…",
self.account.login, delay,
)
time.sleep(delay)
raise ConnectionError(
f"[{self.account.login}] Impossible de se connecter après "
f"{self.max_retries} tentative(s). Dernière erreur : {last_exc}"
) from last_exc
# ── Lecture des dossiers ─────────────────────────────────
def _list_folders(self, client: ProxyIMAPClient) -> List[str]:
return [info[-1] for info in client.list_folders()]
# ── Lecture des messages ─────────────────────────────────
def _read_folder(
self,
client: ProxyIMAPClient,
folder: str,
since: Optional[datetime.datetime] = None,
) -> List[MailResult]:
results: List[MailResult] = []
since = since or datetime.datetime.today()
try:
client.select_folder(folder)
except Exception as exc:
logger.warning("[%s] Impossible d'ouvrir '%s' : %s",
self.account.login, folder, exc)
return results
try:
messages = client.search(['SINCE', since])
except Exception as exc:
logger.warning("[%s] Recherche échouée dans '%s' : %s",
self.account.login, folder, exc)
return results
if not messages:
return results
print("uids {}".format(messages))
logger.info("[%s] %d message(s) dans '%s'",
self.account.login, len(messages), folder)
for uid, msg_data in client.fetch(messages, 'RFC822').items():
try:
raw = msg_data.get(b'RFC822') or msg_data.get('RFC822')
if raw is None:
continue
em = email.message_from_bytes(raw)
from_address = em.get('FROM', '')
subject = em.get('subject', '')
to_addr = em.get('To', self.account.login)
message_id = em.get('Message-ID', '').strip()
body = ""
for part in em.walk():
print(part.get_content_type())
if part.get_content_type() == "text/html":
payload = part.get_payload(decode=True)
if payload:
body = body + payload.decode("utf-8", errors="ignore")
elif part.get_content_type() == "text/plain":
body = body + str(part.get_payload())
logger.info("mail is {} and subject is {}, body is {}".format(
self.account.login, subject, body))
# Filtrer selon les sujets configurés
if not self._subjects or any(s in subject for s in self._subjects):
result = MailResult(
account=self.account.login,
subject=subject,
from_address=from_address,
to_address=to_addr,
body=body,
message_id=message_id,
)
results.append(result)
except Exception as error:
print(error)
print("error trying to read email_message for {}".format(self.account.login))
return results
# ── Point d'entrée public ────────────────────────────────
def read(
self,
since: Optional[datetime.datetime] = None,
skip_folders: Optional[List[str]] = None,
) -> List[MailResult]:
"""
Se connecte au serveur IMAP via le proxy et retourne la liste
des emails de validation trouvés depuis `since` (aujourd'hui par défaut).
Paramètres
----------
since : datetime, optional — date de début de recherche
skip_folders : list[str], optional — dossiers à ignorer
(défaut : ["Sent", "Drafts", "Trash", "Junk", "Spam",
"[Gmail]/All Mail", "[Gmail]/Starred",
"[Gmail]/Important"])
"""
if skip_folders is None:
skip_folders = [
"Sent", "Drafts", "Trash", "Junk", "Spam",
# Dossiers Gmail qui dupliquent le contenu d'INBOX
"[Gmail]/All Mail", "[Gmail]/Starred", "[Gmail]/Important",
]
all_results: List[MailResult] = []
seen_message_ids: set = set() # déduplication inter-dossiers
client = self._connect()
try:
folders = self._list_folders(client)
logger.info("[%s] Dossiers : %s", self.account.login, folders)
for folder in folders:
if folder in skip_folders:
logger.debug("[%s] Dossier ignoré : %s",
self.account.login, folder)
continue
for result in self._read_folder(client, folder, since):
dedup_key = _dedup_key(result)
if dedup_key in seen_message_ids:
logger.debug(
"[%s] Doublon ignoré (clé=%s) dans '%s'",
self.account.login, str(dedup_key)[:40], folder,
)
continue
seen_message_ids.add(dedup_key)
all_results.append(result)
finally:
try:
client.logout()
except Exception:
pass
return all_results
# ──────────────────────────────────────────────────────────────
# Lecture parallèle de plusieurs comptes
# ──────────────────────────────────────────────────────────────
from concurrent.futures import ThreadPoolExecutor, as_completed
def read_multiple_accounts(
accounts: List[MailAccount],
proxy: ProxyConfig,
since: Optional[datetime.datetime] = None,
max_workers: int = 10,
timeout: float = 30.0,
) -> List[MailResult]:
"""
Lit plusieurs comptes email en parallèle via le même proxy.
Retourne la liste consolidée de tous les MailResult trouvés.
"""
all_results: List[MailResult] = []
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_map = {
executor.submit(
ProxyMailReader(acc, proxy, timeout).read, since
): acc.login
for acc in accounts
}
for future in as_completed(future_map):
login = future_map[future]
try:
results = future.result()
logger.info("[%s] %d email(s) de validation récupéré(s).",
login, len(results))
all_results.extend(results)
except Exception as exc:
logger.error("[%s] Erreur : %s", login, exc)
return all_results
# ──────────────────────────────────────────────────────────────
# Point d'entrée — exemple d'utilisation
# ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(message)s",
datefmt="%H:%M:%S",
)
# ── 1. Configurer le proxy ───────────────────────────────
proxy = ProxyConfig(
host=os.environ.get("GMX_PROXY_HOST", ""),
port=int(os.environ.get("GMX_PROXY_PORT", "443")),
proxy_type=os.environ.get("GMX_PROXY_TYPE", "SOCKS5"),
username=os.environ.get("GMX_PROXY_USERNAME"),
password=os.environ.get("GMX_PROXY_PASSWORD"),
)
# ── 2. Définir les comptes à lire ────────────────────────
accounts = [
MailAccount(login="birgitnaya@gmx.net", password="XEeUF3Y1yaO"),
# MailAccount(login="user@gmail.com", password="apppassword"),
# MailAccount(login="user@outlook.com", password="password"),
]
# ── 3. Lancer la lecture ─────────────────────────────────
results = read_multiple_accounts(
accounts=accounts,
proxy=proxy,
since=datetime.datetime.today(),
max_workers=5,
timeout=30.0,
)
# ── 4. Afficher les résultats ────────────────────────────
print(f"\n{'=' * 60}")
print(f" {len(results)} email(s) de validation trouvé(s)")
print(f"{'=' * 60}\n")
for r in results:
print(f" Compte : {r.account}")
print(f" De : {r.from_address}")
print(f" Sujet : {r.subject}")
print(f" URLs : {r.validation_urls or 'aucune'}")
print(f" {'-' * 56}")
+2 -2
View File
@@ -126,5 +126,5 @@ def check_mails():
if __name__ == '__main__':
# remove_invalid_email()
check_mails()
remove_invalid_email()
# check_mails()
+47 -21
View File
@@ -11,11 +11,13 @@ 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.mail_constants import create_imap, show_folders
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.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'
@@ -26,6 +28,23 @@ 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,
subjects: list = None,
) -> None:
account = MailAccount(login=mail.mail, password=mail.password)
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
mail_pojo.to_address = result.to_address or mail.mail
mail_pojo.isImapClient = True
mails_messages.append(mail_pojo)
class MailConfirmationReader():
def __init__(self, login, password):
self.login = login
@@ -147,7 +166,6 @@ 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)
@@ -156,13 +174,20 @@ def accept_appointment_found(accepted_result_list: list):
result.account_password = user.password
mailer.send_email(result, to_all=True)
MONGO_STORE_MANAGER.update_reserve_result(reserve.id, ResultEnum.ACCEPTED, reserve.message)
# sginal.send_result(result)
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:
@@ -171,23 +196,29 @@ def find_confirmation_contacts_for_today():
for _item in _all_appointments_today:
for _mail in _all_mail_list:
if _mail.mail == _item.mail:
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:
# if _item.url_validated is True:
_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 = [MailAddress("saigecong1990@pissmail.com", "cvExXKOP8oY1D@")]
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 DOMAIN_HOTMAIL not in mail.mail:
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, subjects)
else:
mail_reader = MailConfirmationReader(mail.mail, mail.password)
executor.submit(mail_reader.read_emails, mails_messages)
accepted_appointment_list = []
@@ -195,7 +226,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:
@@ -223,17 +254,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 = [CONFIRMATION_SUBJECT_FR, CONFIRMATION_SUBJECT_EN]):
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.reverse()
# excel_reader = ExcelHelper()
# mail_list =excel_reader.read_email_pojo(file_name="/Users/lpan/Desktop/hotmail_list.xlsx")
# mail_address3 = MailAddress(mail="taibenchragu1978@onet.pl", password="2J)kyfNgyOZ")
# mail_list = [mail_address3]
mail_list = find_confirmation_contacts_for_today(mode=mode)
return find_confirmation_contacts_mail_list(mail_list, subjects=subjects)
# init_logger()
@@ -244,6 +270,6 @@ if __name__ == '__main__':
# read_mails_and_find_confirmation_contacts()
_mail_list_today = find_confirmation_contacts_for_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()
# accept_appointment_found([random.choice(_items)])
+17
View File
@@ -27,6 +27,23 @@ DOMAIN_NAVER = "naver.com"
DOMAIN_INBOX_LV = "inbox.lv"
DOMAIN_GMX_DE = "gmx.de"
GMX_DOMAINS = {
DOMAIN_GMX,
DOMAIN_GMX_NET,
DOMAIN_GMX_AT,
DOMAIN_GMX_FR,
DOMAIN_GMX_US,
DOMAIN_GMX_SG,
DOMAIN_GMX_CH,
DOMAIN_GMX_PT,
DOMAIN_GMX_DE,
}
def is_gmx_address(login: str) -> bool:
return any(domain in login for domain in GMX_DOMAINS)
DOMAIN_PISS_MAIL = "pissmail.com"
DOMAIN_INCEL_EMAIL = "incel.email"
DOMAIN_SHITPOSTING_EXPERT = "shitposting.expert"
+35 -1
View File
@@ -1,6 +1,8 @@
import datetime
import email
import re
import os
from typing import Optional
from builtins import list
from concurrent.futures import ThreadPoolExecutor
from datetime import time
@@ -11,7 +13,8 @@ from imapclient import IMAPClient
from src.db.mongo_manager import MONGO_STORE_MANAGER
from src.logs.AppLogging import init_logger
from src.mail.mail_constants import create_imap, show_folders
from src.mail.mail_constants import create_imap, show_folders, is_gmx_address
from src.mail.imap_proxy_reader import ProxyMailReader, ProxyConfig, MailAccount
from src.pojo.mail.mail_pojo import MailPojo
from src.utils.timeutiles import is_time_between
@@ -51,6 +54,33 @@ def find_from_mail(param):
return from_address.strip(" ").strip(">").strip("<")
def get_gmx_proxy_config() -> Optional[ProxyConfig]:
host = os.environ.get("GMX_PROXY_HOST", "")
if not host:
return None
try:
port = int(os.environ.get("GMX_PROXY_PORT", "443"))
except ValueError:
port = 443
return ProxyConfig(
host=host,
port=port,
proxy_type=os.environ.get("GMX_PROXY_TYPE", "SOCKS5"),
username=os.environ.get("GMX_PROXY_USERNAME"),
password=os.environ.get("GMX_PROXY_PASSWORD"),
)
def read_gmx_proxy_emails(mail, mails_messages: list, proxy_config: ProxyConfig) -> None:
account = MailAccount(login=mail.mail, password=mail.password)
results = ProxyMailReader(account, proxy_config).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
mail_pojo.to_address = result.to_address or mail.mail
mails_messages.append(mail_pojo)
class MailReader():
def __init__(self, login, password):
self.login = login
@@ -241,6 +271,7 @@ def read_mails():
if is_time_between(time(7, 30), time(23, 30)):
# get email address
mail_list = MONGO_STORE_MANAGER.get_destination_emails()
gmx_proxy_config = get_gmx_proxy_config()
# excel_reader = ExcelHelper()
# mail_list = excel_reader.read_email_pojo(file_name="/Users/panlei/Downloads/hotmail_list.xlsx")
# mail_address1 = MailAddress(mail="casandrakaamv@onet.pl", password="8F0o0APeAp0z")
@@ -251,6 +282,9 @@ def read_mails():
for mail in mail_list:
# check whether we need to read mail
if need_to_check_email(mail.mail, successful_items):
if is_gmx_address(mail.mail) and gmx_proxy_config is not None:
executor.submit(read_gmx_proxy_emails, mail, mails_messages, gmx_proxy_config)
else:
mail_reader = MailReader(mail.mail, mail.password)
executor.submit(mail_reader.read_emails, mails_messages)
# get ip_country info
+12 -35
View File
@@ -11,8 +11,8 @@ from imapclient import IMAPClient
from src.db.mongo_manager import MONGO_STORE_MANAGER
from src.logs.AppLogging import init_logger
from src.mail.mail_constants import DOMAIN_HOTMAIL, create_imap
from src.mail.mail_reader import need_to_valid_url
from src.mail.mail_constants import DOMAIN_HOTMAIL, create_imap, is_gmx_address
from src.mail.mail_reader import need_to_valid_url, get_gmx_proxy_config, read_gmx_proxy_emails
from src.pojo.mail.mail_pojo import MailPojo
from src.utils.excel_reader import read_contacts
from src.utils.timeutiles import is_time_between
@@ -262,6 +262,7 @@ def read_all_mails(contact_to_book_list=None):
if contact_to_book_list is None:
contact_to_book_list = MONGO_STORE_MANAGER.get_all_contact_to_book_list()
mail_list = MONGO_STORE_MANAGER.get_destination_emails()
gmx_proxy_config = get_gmx_proxy_config()
mail_list_to_check = []
for contact in contact_to_book_list:
for mail in mail_list:
@@ -273,6 +274,9 @@ def read_all_mails(contact_to_book_list=None):
for mail in mail_list_to_check:
# check whether we need to read mail
if need_to_check_email(mail.mail, successful_items):
if is_gmx_address(mail.mail) and gmx_proxy_config is not None:
executor.submit(read_gmx_proxy_emails, mail, mails_messages, gmx_proxy_config)
else:
mail_reader = MailReader(mail.mail, mail.password)
executor.submit(mail_reader.read_emails, mails_messages)
# 在读邮件时候,可能会有其他的约会提交或者约会的链接确认,所以需要刷新一下成功的列表
@@ -300,56 +304,29 @@ if __name__ == '__main__':
contact_to_book_list = read_contacts(
# file_name="/Users/lpan/Desktop/contact_list_2025-01-16_yahoo_100.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-15.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-24.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-15.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-21.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-20.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-14.xlsx")
# file_name="/Users/lpan/Desktop/contact_aol_200_2025-01-15.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-18.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-17.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-19.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-15.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-01-25.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-13.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-12.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-11.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-04.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-10.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-09.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-06.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-07.xlsx")
# file_name="/Users/lpan/Desktop/extracted_yahoo_contacts_129_24_03_win.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-04-02.xlsx")
# file_name="/Users/lpan/Desktop/extracted_aol_contacts_292_24_03_mac.xlsx")
# file_name="/Users/lpan/Desktop/real_name_contacts_197_24_03.xlsx")x
# file_name="/Users/lpan/Desktop/contact_list_2025-03-29.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-31.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_all_21.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_all_10.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_all_14.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_all_19.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_all_22.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_all_2_win.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-29.xlsx")
# file_name="/Users/lpan/Desktop/17_18_04_to_test_win.xlsx")
# file_name="/Users/lpan/Desktop/real_name_contacts_200_aol_win.xlsx")
# file_name="/Users/lpan/Desktop/real_name_contacts_200_aol_mac.xlsx")
# file_name="/Users/lpan/Desktop/real_name_contacts_500_27_03_25_fixed_mac.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-28_mac.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-27_mac.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-27_win.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_all_1.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_all_12.xlsx")
# file_name="/Users/panlei/Desktop/contact_list_2025-04-08.xlsx")
# file_name="/Users/panlei/Desktop/contact_list_2025-04-10.xlsx")
# file_name="/Users/panlei/Desktop/contact_list_2024-10-02.xlsx")
file_name="/Users/panlei/Desktop/contact_list_2025-08-18_no_ms.xlsx")
# file_name="/Users/panlei/Desktop/real_name_contacts_200_gmx_ch_8_04_mac.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-04-05.xlsx")
# file_name="/Users/lpan/Desktop/real_name_contacts_400_aol_mac.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-04-07.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-04-05.xlsx")
# file_name="/Users/lpan/Desktop/real_name_contacts_100_gmx_ch_05_04_mac.xlsx")
file_name="/Users/lpan/Desktop/real_name_contacts_292_win.xlsx")
# file_name="/Users/lpan/Desktop/real_name_contacts_292_win.xlsx")
# file_name="/Users/lpan/Desktop/real_name_contacts_400_mac_04_04.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-04-01.xlsx")
# file_name="/Users/lpan/Desktop/contact_list_2025-03-08_aol_400.xlsx")
read_all_mails(contact_to_book_list=contact_to_book_list)
# read_all_mails()
+7 -7
View File
@@ -1,18 +1,18 @@
from mrz.generator.td1 import TD1CodeGenerator
first_name = "eryan"
last_name = "dai"
first_name = "kele"
last_name = "mi"
document_number = "XKJ0WSK30"
birth_day = "951211"
# sex = "F"
sex = "M"
birth_day = "991125"
sex = "F"
# sex = "M"
# optinal_data = "MFMLMANK<<<<A9" #14位
nationality = "CHN"
country_code = "FRA"
# optinal_data = "<E10805074" # 14位
optinal_data = "<967343145" #总共11位,最前面那位为空,所以加<
optinal_data = "<570873213" #总共11位,最前面那位为空,所以加<
document_prefix = "IR"
expire_date = "260815"
expire_date = "290818"
if optinal_data is not None:
code = TD1CodeGenerator(document_prefix, country_code, document_number, birth_day, sex, expire_date, nationality,
last_name, first_name, optional_data1=optinal_data)
+77 -8
View File
@@ -16,7 +16,7 @@ DEFAULT_SERIAL_TO_IGNORE = ["47e7e36b", "bitbrowser"]
def upload_contacts_list():
_contacts_to_book = read_contacts(str(Path.home()) + "/Desktop/contact_list_2025-05-20.xlsx")
_contacts_to_book = read_contacts(str(Path.home()) + "/Desktop/contact_list_2026-04-11_FIXED.xlsx")
return _contacts_to_book
@@ -119,7 +119,7 @@ def write_new_contacts_to_excel(valid_contacts: list, file_name=str(datetime.dat
def generate_valid_contact_list_for_day(segment_number=1):
_collection_name = "2026-03-26"
_collection_name = "2026-04-11"
_valid_contact_list = MONGO_STORE_MANAGER.get_all_successful_items_for_one_day(_collection_name)
_all_contacts = MONGO_STORE_MANAGER.get_all_contacts_to_book()
_contact_to_save = []
@@ -131,9 +131,11 @@ def generate_valid_contact_list_for_day(segment_number=1):
if _true_contact.mail == _contact.mail:
_contact.last_name = _true_contact.last_name
_contact.phone = _true_contact.phone
_contact.passport = str(_true_contact.resident_card_number)[:9]
_contact.passport = str(_true_contact.passport)[:9]
_contact.first_name = _true_contact.first_name
_contact.resident_card_number = str(_true_contact.resident_card_number)[:9]
_contact.resident_card_number = str(_true_contact.passport)[:9]
if _contact.mail == "angielovato14903@yahoo.com":
print("no resident card number for " + _contact.mail)
print("{}:{}".format(_true_contact.mail, _true_contact.source_from))
if isinstance(_true_contact.source_from, str) and _true_contact.source_from is not None and len(_true_contact.source_from) > 0:
print(_true_contact.source_from)
@@ -285,14 +287,80 @@ def write_resident_card_number_to_contact_list(file_to_read, file_name="contact_
write_list_with_segment_number(file_name, _contacts_to_book, 1)
def check_resident_card_number(file_path):
"""读取 contact_list Excel 文件,检查 resident_card_number 是否为 9 位纯数字字符串。
若不是则输出该联系人信息,并调用 generate_single_titre_sejour_number() 生成新值进行修复。
最终将所有联系人(含修复结果)写入原文件名+_FIXED 的新文件,保持原有列格式。"""
_contact_list = read_contacts(file_path)
_has_invalid = False
for _contact in _contact_list:
rcn = str(_contact.resident_card_number) if _contact.resident_card_number is not None else ""
if not (len(rcn) == 9 and rcn.isdigit()):
print(_contact)
_contact.resident_card_number = generate_single_titre_sejour_number()
_has_invalid = True
if not _has_invalid:
print("[OK] Tous les resident_card_number sont valides (9 chiffres). Aucun fichier créé.")
return
# Construire le chemin du fichier de sortie : même dossier, nom + _FIXED + extension
p = Path(file_path)
output_file = str(p.parent / (p.stem + "_FIXED" + p.suffix))
# Écriture dans le même format que write_new_contacts_to_excel
row = 0
col = 0
workbook = xlsxwriter.Workbook(output_file, {'nan_inf_to_errors': True})
header_data = ['name', 'phone', 'passport', 'email', 'store', 'serial', 'ip_country', 'ua',
'resident_card_number', 'source_from']
worksheet = workbook.add_worksheet()
header_format = workbook.add_format({'bold': True})
for col_num, data in enumerate(header_data):
worksheet.write(row, col_num, data, header_format)
row = 1
def safe_write_val(row_num, col_num, value):
try:
if isinstance(value, str):
if value.lower() in ['nan', 'inf', '-inf']:
worksheet.write(row_num, col_num, "")
else:
worksheet.write(row_num, col_num, value)
elif isinstance(value, (int, float)):
if math.isnan(value) or math.isinf(value):
worksheet.write(row_num, col_num, "")
else:
worksheet.write(row_num, col_num, value)
else:
worksheet.write(row_num, col_num, value if value is not None else "")
except (TypeError, ValueError):
worksheet.write(row_num, col_num, "")
for info in _contact_list:
worksheet.write(row, col, "{} {}".format(info.last_name, info.first_name))
worksheet.write(row, col + 1, info.phone)
worksheet.write(row, col + 2, info.passport)
worksheet.write(row, col + 3, info.mail)
worksheet.write(row, col + 4, info.store)
worksheet.write(row, col + 5, info.serial)
worksheet.write(row, col + 6, info.ip_country)
safe_write_val(row, col + 7, info.ua)
worksheet.write(row, col + 8, info.resident_card_number)
worksheet.write(row, col + 9, info.source_from)
row += 1
workbook.close()
print("Fichier corrigé écrit dans : " + output_file)
if __name__ == '__main__':
# write_resident_card_number_to_contact_list(file_to_read=str(Path.home()) + "/Desktop/contact_list_all_13.xlsx",
# file_name="contact_list_all_13")
# contacts_to_book = upload_contacts_list()
# MONGO_STORE_MANAGER.upload_contact_list(contacts_to_book)
contacts_to_book = upload_contacts_list()
MONGO_STORE_MANAGER.upload_contact_list(contacts_to_book)
# print("start at {}".format(datetime.datetime.now()))
generate_valid_contact_list_for_day(segment_number=2)
# generate_contact_from_mail_list("/Users/lpan/Downloads/邮箱及密码_23_03_25_yahoo.xlsx")
# generate_valid_contact_list_for_day(segment_number=2)
# generate_contact_from_mail_list("/Users/panlei/Downloads/100_yahoo_11_04.xlsx")
# print("end at {}".format(datetime.datetime.now()))
# update_contact_list_not_received_mail()
# get_old_validated_contact_list()
@@ -301,4 +369,5 @@ if __name__ == '__main__':
# merge_contact_list_files(
# "/Users/lpan/Desktop/contact_list_2024-11-06.xlsx"
# ])
# check_resident_card_number(str(Path.home()) + "/Desktop/contact_list_2026-04-11_FIXED.xlsx")
# fix_phone_number_format(str(Path.home()) + "/Desktop/gmx_ch_100_2024-06-13.xlsx")
+10
View File
@@ -0,0 +1,10 @@
import math
def sanitize_excel_value(value):
if value is None:
return ""
if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
return ""
return value
+2
View File
@@ -0,0 +1,2 @@
export PORT=4000
litellm --config config.yaml