no message

This commit is contained in:
2025-12-06 21:25:27 +03:00
parent ce12f23426
commit 03a64d6263
29 changed files with 201 additions and 280 deletions

View File

@@ -17,6 +17,7 @@ class Context(metaclass=_SingletonMeta):
def __init__(self):
# будет вызван только при первом создании
self.context = {}
self.email_client = None
def clear_context(self):
"""Очищает self.context, устанавливая его в None или пустой словарь"""

View File

@@ -11,38 +11,13 @@ from email.header import decode_header
import imaplib
import smtplib
from .objects import EmailMessage, EmailAttachment
# from .objects import EmailMessage, EmailAttachment
class EmailClient:
"""
Класс для работы с электронной почтой по протоколам IMAP и SMTP.
Пример использования:
client = EmailClient(
imap_host='imap.gmail.com',
smtp_host='smtp.gmail.com',
email='your_email@gmail.com',
password='your_password'
)
# Получить новые письма
new_emails = client.get_emails()
# Отправить письмо
msg = EmailMessage(
from_addr='sender@example.com',
subj='Test',
dt=datetime.now(),
body='Hello!',
attachments=[]
)
client.send_email(msg, to_addr='recipient@example.com')
"""
def __init__(self, imap_host: str, smtp_host: str, email: str, password: str,
imap_port: int = 993, smtp_port: int = 587):
self.imap_host = imap_host
self.smtp_host = smtp_host
self.email = email
@@ -52,13 +27,13 @@ class EmailClient:
self.imap_conn = None
def connect(self):
"""Установить IMAP соединение"""
"""Установkение IMAP соединения"""
if self.imap_conn is None:
self.imap_conn = imaplib.IMAP4_SSL(self.imap_host, self.imap_port)
self.imap_conn.login(self.email, self.password)
def disconnect(self):
"""Закрыть IMAP соединение"""
"""Закрытие IMAP соединения"""
if self.imap_conn:
try:
self.imap_conn.disconnect()
@@ -67,7 +42,48 @@ class EmailClient:
pass
self.imap_conn = None
def _decode_header(self, header_value: str) -> str:
def __enter__(self):
"""Поддержка контекстного менеджера"""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Поддержка контекстного менеджера"""
self.disconnect()
def get_emails_id(self, folder: str = "INBOX", only_unseen: bool = True) -> List[int]:
"""Получить список новых электронных писем."""
self.connect()
self.imap_conn.select(folder, readonly=False)
# Ищем письма
search_criteria = "(UNSEEN)" if only_unseen else "ALL"
status, messages = self.imap_conn.search(None, search_criteria)
# ToDo сделать обработку ошибок, подумать нужна ли она!
if status != "OK":
return []
email_ids = messages[0].split()
return email_ids
def get_email(self, email_id, mark_as_read: bool = True):
"""Получить список новых электронных писем."""
self.connect()
status, msg_data = self.imap_conn.fetch(email_id, "(RFC822)")
if status != "OK":
pass
# Парсим письмо
raw_email = msg_data[0][1]
msg = email.message_from_bytes(raw_email)
# Помечаем письмо как прочитанное
if mark_as_read:
self.imap_conn.store(email_id, '+FLAGS', '\\Seen')
return msg
def decode_header(self, header_value: str) -> str:
"""Декодировать заголовок письма."""
if header_value is None:
return ""
@@ -86,213 +102,3 @@ class EmailClient:
decoded_parts.append(str(part))
return ''.join(decoded_parts)
def _extract_body(self, msg: email.message.Message) -> str:
"""Извлечь текст письма из любого типа содержимого, кроме вложений"""
body = ""
if msg.is_multipart():
for part in msg.walk():
content_disposition = str(part.get("Content-Disposition", ""))
# Пропускаем вложения
if "attachment" in content_disposition.lower():
continue
try:
charset = part.get_content_charset() or 'utf-8'
payload = part.get_payload(decode=True)
if payload:
body_piece = payload.decode(charset, errors='ignore')
body += body_piece
except Exception:
pass
else:
try:
charset = msg.get_content_charset() or 'utf-8'
payload = msg.get_payload(decode=True)
if payload:
body = payload.decode(charset, errors='ignore')
except Exception:
pass
return body
def __extract_email(self, text: str) -> str:
match = re.search(r'<([^<>]+)>', text)
if match:
return match.group(1)
return None
def _extract_first_sender(self, body: str):
"""Извлекает адреса отправителей из пересылаемого сообщения. Нужно для отладки"""
# Ищем email внутри скобок после строки "Пересылаемое сообщение"
pattern = r"Пересылаемое сообщение.*?\((.*?)\)"
match = re.search(pattern, body, re.DOTALL)
if match:
return match.group(1)
return None
def _extract_attachments(self, msg: email.message.Message) -> List[EmailAttachment]:
"""Извлечь вложения из письма."""
attachments = []
for part in msg.walk():
content_disposition = str(part.get("Content-Disposition", ""))
if "attachment" in content_disposition:
filename = part.get_filename()
if filename:
# Декодируем имя файла
filename = self._decode_header(filename)
# Получаем содержимое
content = part.get_payload(decode=True)
if content:
attachments.append(EmailAttachment(filename=filename, content=content))
return attachments
def get_emails_id(self, folder: str = "INBOX", only_unseen: bool = True, mark_as_read: bool = True) -> List[
EmailMessage]:
"""Получить список новых электронных писем."""
self.connect()
# Выбираем папку
self.imap_conn.select(folder, readonly=False)
# Ищем письма
search_criteria = "(UNSEEN)" if only_unseen else "ALL"
status, messages = self.imap_conn.search(None, search_criteria)
if status != "OK":
return []
email_ids = messages[0].split()
return email_ids
def get_emails(self, folder: str = "INBOX", only_unseen: bool = True, mark_as_read: bool = True) -> List[EmailMessage]:
"""Получить список новых электронных писем."""
self.connect()
# Выбираем папку
self.imap_conn.select(folder, readonly=False)
# Ищем письма
search_criteria = "(UNSEEN)" if only_unseen else "ALL"
status, messages = self.imap_conn.search(None, search_criteria)
if status != "OK":
return []
email_ids = messages[0].split()
emails = []
for email_id in email_ids:
try:
# Получаем письмо
status, msg_data = self.imap_conn.fetch(email_id, "(RFC822)")
if status != "OK":
continue
# Парсим письмо
raw_email = msg_data[0][1]
msg = email.message_from_bytes(raw_email)
# Извлекаем данные
from_addr = self._decode_header(msg.get("From", ""))
subject = self._decode_header(msg.get("Subject", ""))
from_email = self.__extract_email(from_addr)
# Получаем дату
date_str = msg.get("Date", "")
try:
date_tuple = email.utils.parsedate_tz(date_str)
if date_tuple:
timestamp = email.utils.mktime_tz(date_tuple)
dt = datetime.fromtimestamp(timestamp)
else:
dt = datetime.now()
except:
dt = datetime.now()
# Извлекаем тело письма
body = self._extract_body(msg)
#print(body)
first_sender = self._extract_first_sender(body)
# Извлекаем вложения
attachments = self._extract_attachments(msg)
# Создаем объект письма
email_obj = EmailMessage(
from_addr=from_addr,
from_email=from_email,
subj=subject,
dt=dt,
body=body,
attachments=attachments,
first_sender=first_sender
)
emails.append(email_obj)
# Помечаем письмо как прочитанное
if mark_as_read:
self.imap_conn.store(email_id, '+FLAGS', '\\Seen')
except Exception as e:
print(f"Ошибка при обработке письма {email_id}: {e}")
continue
return emails
def send_email(self, message: EmailMessage, to_addr: str, cc: Optional[List[str]] = None, bcc: Optional[List[str]] = None):
"""Отправить электронное письмо"""
# Создаем multipart сообщение
msg = MIMEMultipart()
msg['From'] = self.email
msg['To'] = to_addr
msg['Subject'] = message.subj
if cc:
msg['Cc'] = ', '.join(cc)
# Добавляем тело письма
msg.attach(MIMEText(message.body, 'plain', 'utf-8'))
# Добавляем вложения
for attachment in message.attachments:
part = MIMEBase('application', 'octet-stream')
part.set_payload(attachment.content)
encoders.encode_base64(part)
part.add_header(
'Content-Disposition',
f'attachment; filename= {attachment.filename}'
)
msg.attach(part)
# Формируем список всех получателей
recipients = [to_addr]
if cc:
recipients.extend(cc)
if bcc:
recipients.extend(bcc)
# Отправляем письмо
with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
server.starttls()
server.login(self.email, self.password)
server.sendmail(self.email, recipients, msg.as_string())
def __enter__(self):
"""Поддержка контекстного менеджера"""
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Поддержка контекстного менеджера"""
self.disconnect()

View File

@@ -0,0 +1,99 @@
import re
from datetime import datetime
from typing import List, Optional
from dataclasses import dataclass
import email
from email import encoders
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.header import decode_header
import imaplib
import smtplib
class EmailMagic:
def __init__(self, email):
self.email = email
def _decode_header(self, header_value: str) -> str:
"""Декодировать заголовок письма."""
if header_value is None:
return ""
decoded_parts = []
for part, encoding in decode_header(header_value):
if isinstance(part, bytes):
if encoding:
try:
decoded_parts.append(part.decode(encoding))
except:
decoded_parts.append(part.decode('utf-8', errors='ignore'))
else:
decoded_parts.append(part.decode('utf-8', errors='ignore'))
else:
decoded_parts.append(str(part))
return ''.join(decoded_parts)
def _extract_body(self, msg: email.message.Message) -> str:
"""Извлечь текст письма из любого типа содержимого, кроме вложений"""
body = ""
if msg.is_multipart():
for part in msg.walk():
content_disposition = str(part.get("Content-Disposition", ""))
# Пропускаем вложения
if "attachment" in content_disposition.lower():
continue
try:
charset = part.get_content_charset() or 'utf-8'
payload = part.get_payload(decode=True)
if payload:
body_piece = payload.decode(charset, errors='ignore')
body += body_piece
except Exception:
pass
else:
try:
charset = msg.get_content_charset() or 'utf-8'
payload = msg.get_payload(decode=True)
if payload:
body = payload.decode(charset, errors='ignore')
except Exception:
pass
return body
def __extract_email(self, text: str) -> str:
match = re.search(r'<([^<>]+)>', text)
if match:
return match.group(1)
return None
def _extract_first_sender(self, body: str):
"""Извлекает адреса отправителей из пересылаемого сообщения. Нужно для отладки"""
# Ищем email внутри скобок после строки "Пересылаемое сообщение"
pattern = r"Пересылаемое сообщение.*?\((.*?)\)"
match = re.search(pattern, body, re.DOTALL)
if match:
return match.group(1)
return None
def _extract_attachments(self, msg: email.message.Message) -> List[EmailAttachment]:
"""Извлечь вложения из письма."""
attachments = []
for part in msg.walk():
content_disposition = str(part.get("Content-Disposition", ""))
if "attachment" in content_disposition:
filename = part.get_filename()
if filename:
# Декодируем имя файла
filename = self._decode_header(filename)
# Получаем содержимое
content = part.get_payload(decode=True)
if content:
attachments.append(EmailAttachment(filename=filename, content=content))
return attachments

View File

@@ -1 +0,0 @@
from .email_processor import EmailProcessor

View File

@@ -0,0 +1 @@
from .processor import EmailProcessor

View File

@@ -1,7 +1,7 @@
import random
import logging
from mail_order_bot.email_handler.handlers.abstract_task import AbstractTask
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
logger = logging.getLogger(__name__)

View File

@@ -1,7 +1,7 @@
import logging
import requests
from mail_order_bot.email_handler.handlers.abstract_task import AbstractTask
from mail_order_bot.email_handler.order.auto_part_order import OrderStatus
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
from mail_order_bot.email_processor.order.auto_part_order import OrderStatus
logger = logging.getLogger(__name__)

View File

@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
from typing import Dict, Any
from mail_order_bot.email_handler.context import Context
from mail_order_bot.context import Context
class AbstractTask(ABC, Context):

View File

@@ -3,8 +3,8 @@ import pandas as pd
from typing import Dict, Any, Optional
from decimal import Decimal
from io import BytesIO
#from mail_order_bot.email_handler.handlers.order_position import OrderPosition
from mail_order_bot.email_handler.handlers.abstract_task import AbstractTask
#from mail_order_bot.email_processor.handlers.order_position import OrderPosition
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
from ...order.auto_part_position import AutoPartPosition

View File

@@ -3,8 +3,8 @@ import pandas as pd
from typing import Dict, Any, Optional
from decimal import Decimal
from io import BytesIO
#from mail_order_bot.email_handler.handlers.order_position import OrderPosition
from mail_order_bot.email_handler.handlers.abstract_task import AbstractTask
#from mail_order_bot.email_processor.handlers.order_position import OrderPosition
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
from ...order.auto_part_position import AutoPartPosition

View File

@@ -1,6 +1,6 @@
import logging
from mail_order_bot.email_handler.handlers.abstract_task import AbstractTask
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
logger = logging.getLogger(__name__)

View File

@@ -1,7 +1,7 @@
import random
import logging
from mail_order_bot.email_handler.handlers.abstract_task import AbstractTask
from mail_order_bot.email_handler.order.auto_part_order import OrderStatus
from mail_order_bot.email_processor.handlers.abstract_task import AbstractTask
from mail_order_bot.email_processor.order.auto_part_order import OrderStatus
from decimal import Decimal
import random
logger = logging.getLogger(__name__)

View File

@@ -6,12 +6,9 @@ from pathlib import Path
logger = logging.getLogger(__name__)
from .order.auto_part_order import AutoPartOrder
from .context import Context
from mail_order_bot.context import Context
from enum import Enum
from .handlers import *
class RequestStatus(Enum):
NEW = "new"
@@ -28,10 +25,20 @@ class EmailProcessor(Context):
self.configs_path = configs_path
self.status = RequestStatus.NEW
def process_email(self, email):
# Очистить контекст
self.context.clear()
def process(self, email_id):
config = self._load_config(client)
# Сохранить письмо в контекст
self.context["email"] = email
# Определить клиента
# Определить конфиг для пайплайна
config = {}
# Запустить обработку пайплайна
for stage in config["pipeline"]:
handler_name = stage["handler"]
logger.info(f"Processing handler: {handler_name}")
@@ -43,5 +50,3 @@ class EmailProcessor(Context):
path = os.path.join(self.configs_path, client + '.yml')
with open(path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def _load_email(self):

View File

@@ -7,39 +7,49 @@ import os
from dotenv import load_dotenv
from email_client import EmailClient
from excel_proceccor import ExcelProcessor
from email_processor import EmailProcessor
from context import Context
logger = logging.getLogger()
class MailOrderBot(ConfigManager):
def __init__(self, *agrs, **kwargs):
super().__init__(*agrs, **kwargs)
# Объявить почтового клиента
self.email_client = EmailClient(
imap_host=os.getenv('IMAP_HOST'),
smtp_host=os.getenv('SMTP_HOST'),
email=os.getenv('EMAIL_USER'),
password=os.getenv('EMAIL_PASSWORD'),
imap_port=os.getenv('IMAP_PORT'),
smtp_port=os.getenv('SMTP_PORT')
imap_port=int(os.getenv('IMAP_PORT', default="993")),
smtp_port=int(os.getenv('SMTP_PORT', default="587")),
)
# Сохранить почтовый клиент в контекст
self.context = Context()
self.context.email_client = self.email_client
# Обработчик писем
self.email_processor = EmailProcessor()
def execute(self):
logger.debug(f"Check emails for new orders")
emails = self.email_client.get_emails(folder="spareparts", only_unseen=True, mark_as_read=True)
# Получить список айдишников письма
unread_email_ids = self.email_client.get_emails_id(folder="spareparts")
logger.info(f"Новых писем - {len(unread_email_ids)}")
for email in emails:
logger.info(email.subj)
logger.info(email.from_addr)
logger.info(email.dt)
logger.info(email.body)
logger.info(email.first_sender)
logger.info('--------------------------------')
logger.critical("mail checked")
# Обработать каждое письмо по идентификатору
for email_id in unread_email_ids:
logger.debug(f"Обработка письма с идентификатором {email_id}")
# Получить письмо по идентификатору и запустить его обработку
email = self.email_client.get_email(email_id)
self.email_processor.process_email(email)
logger = logging.getLogger()

View File

@@ -1,8 +1,8 @@
import os
from dotenv import load_dotenv
from mail_order_bot.abcp_api.abcp_provider import AbcpProvider
from mail_order_bot.email_handler.order.auto_part_order import AutoPartOrder
from mail_order_bot.email_handler.order.auto_part_position import AutoPartPosition
from mail_order_bot.email_processor.order.auto_part_order import AutoPartOrder
from mail_order_bot.email_processor.order.auto_part_position import AutoPartPosition
if __name__ == "__main__":
print(__name__)# подгружаем переменные окружения
load_dotenv()

View File

@@ -1,7 +1,7 @@
import os
import chardet # pip install chardet
import traceback
from mail_order_bot.email_handler import EmailProcessor
from mail_order_bot.email_processor import EmailProcessor
import datetime
# установим рабочую директорию
import os

View File

@@ -1,7 +1,7 @@
import os
import chardet # pip install chardet
import traceback
from mail_order_bot.excel_processor import ExcelProcessor
from mail_order_bot.excel_parcer import ExcelProcessor
# установим рабочую директорию
import os