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): def __init__(self):
# будет вызван только при первом создании # будет вызван только при первом создании
self.context = {} self.context = {}
self.email_client = None
def clear_context(self): def clear_context(self):
"""Очищает self.context, устанавливая его в None или пустой словарь""" """Очищает self.context, устанавливая его в None или пустой словарь"""

View File

@@ -11,38 +11,13 @@ from email.header import decode_header
import imaplib import imaplib
import smtplib import smtplib
from .objects import EmailMessage, EmailAttachment
# from .objects import EmailMessage, EmailAttachment
class EmailClient: 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, def __init__(self, imap_host: str, smtp_host: str, email: str, password: str,
imap_port: int = 993, smtp_port: int = 587): imap_port: int = 993, smtp_port: int = 587):
self.imap_host = imap_host self.imap_host = imap_host
self.smtp_host = smtp_host self.smtp_host = smtp_host
self.email = email self.email = email
@@ -50,15 +25,15 @@ class EmailClient:
self.imap_port = imap_port self.imap_port = imap_port
self.smtp_port = smtp_port self.smtp_port = smtp_port
self.imap_conn = None self.imap_conn = None
def connect(self): def connect(self):
"""Установить IMAP соединение""" """Установkение IMAP соединения"""
if self.imap_conn is None: if self.imap_conn is None:
self.imap_conn = imaplib.IMAP4_SSL(self.imap_host, self.imap_port) self.imap_conn = imaplib.IMAP4_SSL(self.imap_host, self.imap_port)
self.imap_conn.login(self.email, self.password) self.imap_conn.login(self.email, self.password)
def disconnect(self): def disconnect(self):
"""Закрыть IMAP соединение""" """Закрытие IMAP соединения"""
if self.imap_conn: if self.imap_conn:
try: try:
self.imap_conn.disconnect() self.imap_conn.disconnect()
@@ -67,7 +42,48 @@ class EmailClient:
pass pass
self.imap_conn = None 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: if header_value is None:
return "" return ""
@@ -84,215 +100,5 @@ class EmailClient:
decoded_parts.append(part.decode('utf-8', errors='ignore')) decoded_parts.append(part.decode('utf-8', errors='ignore'))
else: else:
decoded_parts.append(str(part)) 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()
return ''.join(decoded_parts)

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 random
import logging 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__) logger = logging.getLogger(__name__)

View File

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

View File

@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Any 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): class AbstractTask(ABC, Context):

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import logging 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__) logger = logging.getLogger(__name__)

View File

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

View File

@@ -6,12 +6,9 @@ from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
from .order.auto_part_order import AutoPartOrder from mail_order_bot.context import Context
from .context import Context
from enum import Enum from enum import Enum
from .handlers import *
class RequestStatus(Enum): class RequestStatus(Enum):
NEW = "new" NEW = "new"
@@ -28,10 +25,20 @@ class EmailProcessor(Context):
self.configs_path = configs_path self.configs_path = configs_path
self.status = RequestStatus.NEW 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"]: for stage in config["pipeline"]:
handler_name = stage["handler"] handler_name = stage["handler"]
logger.info(f"Processing handler: {handler_name}") logger.info(f"Processing handler: {handler_name}")
@@ -42,6 +49,4 @@ class EmailProcessor(Context):
"""Загружает конфигурацию из YAML или JSON""" """Загружает конфигурацию из YAML или JSON"""
path = os.path.join(self.configs_path, client + '.yml') path = os.path.join(self.configs_path, client + '.yml')
with open(path, 'r', encoding='utf-8') as f: with open(path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f) return yaml.safe_load(f)
def _load_email(self):

View File

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

View File

@@ -1,8 +1,8 @@
import os import os
from dotenv import load_dotenv from dotenv import load_dotenv
from mail_order_bot.abcp_api.abcp_provider import AbcpProvider 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_processor.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_position import AutoPartPosition
if __name__ == "__main__": if __name__ == "__main__":
print(__name__)# подгружаем переменные окружения print(__name__)# подгружаем переменные окружения
load_dotenv() load_dotenv()

View File

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

View File

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