rabbitmq implemented and tested
This commit is contained in:
18
ServicesTask/app/services/mail/IsBank/params.py
Normal file
18
ServicesTask/app/services/mail/IsBank/params.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import os
|
||||
|
||||
from ..config import ConfigServices
|
||||
|
||||
class IsBankConfig:
|
||||
|
||||
MAILBOX: str = os.getenv("MAILBOX", "bilgilendirme@ileti.isbank.com.tr")
|
||||
AUTHORIZE_IBAN: str = os.getenv("AUTHORIZE_IBAN", "4245-0093333")
|
||||
NO_ATTACHMENT_FOLDER: str = "NoAttachment"
|
||||
COMPLETED_FOLDER: str = "Completed"
|
||||
SERVICE_NAME: str = "IsBankEmailService"
|
||||
TASK_DATA_PREFIX: str = ConfigServices.MAIN_TASK_PREFIX
|
||||
TASK_MAILID_INDEX_PREFIX: str = ConfigServices.TASK_MAILID_INDEX_PREFIX
|
||||
TASK_UUID_INDEX_PREFIX: str = ConfigServices.TASK_UUID_INDEX_PREFIX
|
||||
TASK_SEEN_PREFIX: str = ConfigServices.TASK_SEEN_PREFIX
|
||||
SERVICE_PREFIX: str = ConfigServices.SERVICE_PREFIX_MAIL_READER
|
||||
NEXT_SERVICE_PREFIX: str = ConfigServices.SERVICE_PREFIX_MAIL_PARSER
|
||||
|
||||
29
ServicesTask/app/services/mail/IsBank/runner.py
Normal file
29
ServicesTask/app/services/mail/IsBank/runner.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import sys
|
||||
|
||||
from time import sleep
|
||||
from logging import getLogger, basicConfig, INFO, StreamHandler, FileHandler
|
||||
|
||||
from ..mail_handler import EmailReaderService
|
||||
from .params import IsBankConfig
|
||||
|
||||
|
||||
format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
handlers = [StreamHandler(sys.stdout), FileHandler('isbank_email_service.log')]
|
||||
basicConfig(level=INFO, format=format, handlers=handlers)
|
||||
logger = getLogger(IsBankConfig.SERVICE_NAME)
|
||||
|
||||
|
||||
def initialize_service():
|
||||
"""Initialize the service with proper error handling"""
|
||||
try:
|
||||
logger.info("Creating EmailReaderService")
|
||||
email_service = EmailReaderService(IsBankConfig())
|
||||
|
||||
logger.info("Connecting to email service")
|
||||
email_service.login_and_connect()
|
||||
return email_service
|
||||
except Exception as e:
|
||||
logger.error(f"Service initialization failed: {str(e)}")
|
||||
sleep(5)
|
||||
return initialize_service()
|
||||
|
||||
155
ServicesTask/app/services/mail/config.py
Normal file
155
ServicesTask/app/services/mail/config.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import os
|
||||
from re import TEMPLATE
|
||||
from pydantic import BaseModel
|
||||
from typing import Any, List, Optional, Union
|
||||
|
||||
|
||||
class FromToHeader(BaseModel):
|
||||
|
||||
display_name: Optional[str]
|
||||
username: Optional[str]
|
||||
domain: Optional[str]
|
||||
mail: Optional[str]
|
||||
|
||||
|
||||
class MailReader(BaseModel):
|
||||
|
||||
id: str
|
||||
subject: str
|
||||
from_: FromToHeader
|
||||
to: List[FromToHeader]
|
||||
date: str
|
||||
body_text: str
|
||||
|
||||
|
||||
class MailParser(BaseModel):
|
||||
|
||||
filename: str
|
||||
content_type: str
|
||||
charset: str
|
||||
data: str
|
||||
|
||||
class FinderIban(BaseModel):
|
||||
|
||||
filename: str
|
||||
iban: str
|
||||
bank_date: str
|
||||
channel_branch: str
|
||||
currency_value: float
|
||||
balance: float
|
||||
additional_balance: float
|
||||
process_name: str
|
||||
process_type: str
|
||||
process_comment: str
|
||||
bank_reference_code: str
|
||||
|
||||
|
||||
class FinderComment(FinderIban):
|
||||
|
||||
build_id: Optional[int] = None
|
||||
build_uu_id: Optional[str] = None
|
||||
decision_book_id: Optional[int] = None
|
||||
decision_book_uu_id: Optional[str] = None
|
||||
|
||||
|
||||
class RedisData(BaseModel):
|
||||
MailReader: MailReader
|
||||
MailParser: List[MailParser]
|
||||
FinderIban: List[FinderIban]
|
||||
FinderComment: List[FinderComment]
|
||||
|
||||
|
||||
class Status:
|
||||
PENDING: str = "PENDING"
|
||||
IN_PROGRESS: str = "IN_PROGRESS"
|
||||
COMPLETED: str = "COMPLETED"
|
||||
FAILED: str = "FAILED"
|
||||
|
||||
|
||||
class RedisTaskObject(BaseModel):
|
||||
task: str
|
||||
data: RedisData
|
||||
completed: bool
|
||||
service: str
|
||||
status: str
|
||||
created_at: str
|
||||
is_completed: bool
|
||||
|
||||
|
||||
class MailSendModel(BaseModel):
|
||||
receivers: List[str]
|
||||
subject: str
|
||||
template_name: str
|
||||
data: dict
|
||||
|
||||
|
||||
class RedisMailSender(BaseModel):
|
||||
task: RedisTaskObject
|
||||
data: MailSendModel
|
||||
completed: bool
|
||||
service: str
|
||||
status: str
|
||||
created_at: str
|
||||
completed: bool
|
||||
|
||||
|
||||
class EmailConfig:
|
||||
|
||||
HOST: str = os.getenv("EMAIL_HOST", "10.10.2.34")
|
||||
USERNAME: str = os.getenv("EMAIL_USERNAME", "isbank@mehmetkaratay.com.tr")
|
||||
PASSWORD: str = os.getenv("EMAIL_PASSWORD", "system")
|
||||
PORT: int = int(os.getenv("EMAIL_PORT", 993))
|
||||
|
||||
@classmethod
|
||||
def as_dict(cls):
|
||||
return dict(host=EmailConfig.HOST, port=EmailConfig.PORT, username=EmailConfig.USERNAME, password=EmailConfig.PASSWORD)
|
||||
|
||||
|
||||
class RedisConfig:
|
||||
|
||||
HOST: str = os.getenv("REDIS_HOST", "10.10.2.15")
|
||||
PASSWORD: str = os.getenv("REDIS_PASSWORD", "your_strong_password_here")
|
||||
PORT: int = int(os.getenv("REDIS_PORT", 6379))
|
||||
DB: int = int(os.getenv("REDIS_DB", 0))
|
||||
|
||||
@classmethod
|
||||
def as_dict(cls):
|
||||
return dict(host=RedisConfig.HOST, port=int(RedisConfig.PORT), password=RedisConfig.PASSWORD, db=int(RedisConfig.DB))
|
||||
|
||||
|
||||
class MailReaderMainConfig:
|
||||
|
||||
MAILBOX: str
|
||||
AUTHORIZE_IBAN: str
|
||||
NO_ATTACHMENT_FOLDER: str
|
||||
COMPLETED_FOLDER: str
|
||||
TASK_DATA_PREFIX: str
|
||||
TASK_MAILID_INDEX_PREFIX: str
|
||||
TASK_UUID_INDEX_PREFIX: str
|
||||
TASK_SEEN_PREFIX: str
|
||||
SERVICE_PREFIX: str
|
||||
NEXT_SERVICE_PREFIX: str
|
||||
|
||||
|
||||
class ConfigServices:
|
||||
|
||||
MAIN_TASK_PREFIX: str = "BANK:SERVICES:TASK:DATA"
|
||||
|
||||
TASK_MAILID_INDEX_PREFIX: str = "BANK:SERVICES:TASK:MAILID"
|
||||
TASK_UUID_INDEX_PREFIX: str = "BANK:SERVICES:TASK:UUID"
|
||||
TASK_SEEN_PREFIX: str = "BANK:SERVICES:TASK:SEEN"
|
||||
TASK_DELETED_PREFIX: str = "BANK:SERVICES:TASK:DELETED"
|
||||
TASK_COMMENT_PARSER: str = "BANK:SERVICES:TASK:COMMENT:PARSER"
|
||||
TASK_PREDICT_RESULT: str = "BANK:SERVICES:TASK:COMMENT:RESULT"
|
||||
|
||||
SERVICE_PREFIX_MAIL_READER: str = "MailReader"
|
||||
SERVICE_PREFIX_MAIL_PARSER: str = "MailParser"
|
||||
SERVICE_PREFIX_FINDER_IBAN: str = "FinderIban"
|
||||
SERVICE_PREFIX_FINDER_COMMENT: str = "FinderComment"
|
||||
SERVICE_PREFIX_MAIL_SENDER: str = "MailSender"
|
||||
|
||||
TEMPLATE_ACCOUNT_RECORDS: str = "template_accounts.html"
|
||||
|
||||
|
||||
paramsRedisData = Union[MailReader, MailParser, FinderIban, FinderComment]
|
||||
|
||||
381
ServicesTask/app/services/mail/mail_handler.py
Normal file
381
ServicesTask/app/services/mail/mail_handler.py
Normal file
@@ -0,0 +1,381 @@
|
||||
import os
|
||||
import socket
|
||||
import logging
|
||||
|
||||
from functools import wraps
|
||||
from base64 import b64encode
|
||||
from time import sleep
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any, Union, TypeVar, Tuple
|
||||
|
||||
from email.message import EmailMessage
|
||||
from email.policy import default as policy
|
||||
from email.headerregistry import UniqueDateHeader, UniqueAddressHeader, UniqueUnstructuredHeader
|
||||
from email.parser import BytesParser
|
||||
from imaplib import IMAP4_SSL, IMAP4
|
||||
|
||||
from .config import EmailConfig, MailReaderMainConfig
|
||||
|
||||
|
||||
logger = logging.getLogger('Email Reader Service')
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def retry_on_connection_error(max_retries: int = 3, delay: int = 5, backoff: int = 2, exceptions=(Exception,)):
|
||||
"""
|
||||
Retry decorator with exponential backoff for handling connection errors
|
||||
|
||||
Args:
|
||||
max_retries: Maximum number of retries
|
||||
delay: Initial delay between retries in seconds
|
||||
backoff: Backoff multiplier
|
||||
exceptions: Tuple of exceptions to catch
|
||||
Returns: Decorated function
|
||||
"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
mtries, mdelay = max_retries, delay
|
||||
while mtries > 0:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except exceptions as e:
|
||||
logger.warning(f"Connection error in {func.__name__}: {str(e)}, retrying in {mdelay}s...")
|
||||
sleep(mdelay)
|
||||
mtries -= 1
|
||||
mdelay *= backoff
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
class Mails:
|
||||
"""Class representing an email with attachments and metadata"""
|
||||
|
||||
def __init__(self, mail_id: bytes, mail_data: bytes):
|
||||
"""
|
||||
Initialize a mail object
|
||||
Args: mail_id: Unique identifier for the email, mail_data: Raw email data
|
||||
"""
|
||||
self.id: bytes = mail_id
|
||||
self.raw_data: bytes = mail_data
|
||||
self.attachments: List[Dict[str, Union[str, bytes]]] = []
|
||||
self.message: EmailMessage = BytesParser(policy=policy).parsebytes(mail_data)
|
||||
self.subject: UniqueUnstructuredHeader = self.message.get('Subject', '') or ''
|
||||
self.from_: UniqueAddressHeader = self.message.get('From', '') or ''
|
||||
self.to: UniqueAddressHeader = self.message.get('To', '') or ''
|
||||
self.date: UniqueDateHeader = self.message.get('Date', '') or ''
|
||||
self.body_text: str = self._get_body_text()
|
||||
self._extract_attachments()
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert mail object to dictionary representation
|
||||
Returns: Dictionary representation of mail
|
||||
"""
|
||||
return {
|
||||
'id': self.id.decode('utf-8'),
|
||||
'attachments': [{
|
||||
'filename': attachment['filename'], 'content_type': attachment['content_type'], 'charset': attachment['charset'],
|
||||
'data': b64encode(attachment['data']).decode(attachment['charset'], errors='replace')
|
||||
} for attachment in self.attachments],
|
||||
'subject': str(self.subject),
|
||||
'from_': {
|
||||
"display_name": self.from_.addresses[0].display_name, "username": self.from_.addresses[0].username,
|
||||
"domain": self.from_.addresses[0].domain, "mail": f"{self.from_.addresses[0].username}@{self.from_.addresses[0].domain}"
|
||||
},
|
||||
'to': [
|
||||
{
|
||||
"display_name": address.display_name, "username": address.username, "domain": address.domain,
|
||||
"mail": f"{address.username}@{address.domain}" } for address in self.to.addresses
|
||||
], 'date': str(self.date.datetime), 'body_text': str(self.body_text)
|
||||
}
|
||||
|
||||
def _get_body_text(self) -> str:
|
||||
"""
|
||||
Extract plain text body from email
|
||||
Returns: Plain text body of email
|
||||
"""
|
||||
body = self.message.get_body(preferencelist=('plain',))
|
||||
if body is not None:
|
||||
return body.get_content() or ''
|
||||
if self.message.is_multipart():
|
||||
for part in self.message.walk():
|
||||
if part.get_content_type() == 'text/plain' and (part.get_content_disposition() or '') != 'attachment':
|
||||
try:
|
||||
return part.get_content() or ''
|
||||
except Exception:
|
||||
payload = part.get_payload(decode=True) or b''
|
||||
return payload.decode(part.get_content_charset() or 'utf-8', errors='replace')
|
||||
else:
|
||||
if self.message.get_content_type() == 'text/plain':
|
||||
try:
|
||||
return self.message.get_content() or ''
|
||||
except Exception:
|
||||
payload = self.message.get_payload(decode=True) or b''
|
||||
return payload.decode(self.message.get_content_charset() or 'utf-8', errors='replace')
|
||||
return ''
|
||||
|
||||
def _extract_attachments(self) -> None:
|
||||
"""Extract attachments from email"""
|
||||
for part in self.message.walk():
|
||||
if part.get_content_disposition() == 'attachment':
|
||||
filename = part.get_filename()
|
||||
if not filename:
|
||||
continue
|
||||
data = part.get_payload(decode=True) or b''
|
||||
charset = part.get_charset() or 'utf-8'
|
||||
self.attachments.append({'filename': filename, 'content_type': part.get_content_type(), 'data': data, 'charset': charset})
|
||||
|
||||
def save_attachments(self, folder: str) -> None:
|
||||
"""
|
||||
Save attachments to folder
|
||||
Args: folder: Folder to save attachments to
|
||||
"""
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
for att in self.attachments:
|
||||
with open(os.path.join(folder, att['filename']), 'wb') as f:
|
||||
f.write(att['data'])
|
||||
|
||||
|
||||
class EmailReaderService:
|
||||
|
||||
"""Service for reading emails from mailbox with improved connection resilience"""
|
||||
|
||||
def __init__(self, config: MailReaderMainConfig):
|
||||
"""
|
||||
Initialize email reader service
|
||||
Args: config: Application configuration
|
||||
"""
|
||||
self.email_config = EmailConfig()
|
||||
self.config = config
|
||||
self.mail = None
|
||||
self.data: List[Mails] = []
|
||||
self.mail_count = 0
|
||||
self.is_connected = False
|
||||
self.connect_imap()
|
||||
|
||||
def connect_imap(self) -> bool:
|
||||
"""
|
||||
Establish IMAP connection with retry mechanism
|
||||
Returns: True if connection successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if self.mail:
|
||||
try:
|
||||
self.mail.close()
|
||||
self.mail.logout()
|
||||
except Exception:
|
||||
pass
|
||||
logger.info(f"Connecting to IMAP server {self.email_config.HOST}:{self.email_config.PORT}")
|
||||
self.mail = IMAP4_SSL(self.email_config.HOST, self.email_config.PORT)
|
||||
self.is_connected = True
|
||||
return True
|
||||
except (socket.error, IMAP4.error) as e:
|
||||
logger.error(f"Failed to connect to IMAP server: {str(e)}")
|
||||
self.is_connected = False
|
||||
return False
|
||||
|
||||
@retry_on_connection_error(max_retries=3, delay=5, exceptions=(socket.error, IMAP4.error, OSError))
|
||||
def login_and_connect(self) -> bool:
|
||||
"""
|
||||
Login to IMAP server and connect to inbox with retry mechanism
|
||||
Returns: True if login successful, False otherwise
|
||||
Raises: ConnectionError: If connection cannot be established
|
||||
"""
|
||||
if not self.is_connected:
|
||||
if not self.connect_imap():
|
||||
raise ConnectionError("Cannot establish connection to IMAP server")
|
||||
|
||||
try:
|
||||
logger.info(f"Logging in as {self.email_config.USERNAME}")
|
||||
self.mail.login(self.email_config.USERNAME, self.email_config.PASSWORD)
|
||||
self._connect_inbox()
|
||||
logger.info("Successfully logged in and connected to inbox")
|
||||
return True
|
||||
except (socket.error, IMAP4.error) as e:
|
||||
logger.error(f"Login failed: {str(e)}")
|
||||
self.is_connected = False
|
||||
raise
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=3, exceptions=(socket.error, IMAP4.error, OSError))
|
||||
def refresh(self) -> Tuple[List[Mails], int, int]:
|
||||
"""
|
||||
Refresh mail data with connection retry
|
||||
Returns: Tuple of (mail data, mail count, data length)
|
||||
"""
|
||||
try:
|
||||
self.mail_count = self._fetch_count()
|
||||
self.data = self._fetch_all()
|
||||
return self.data, self.mail_count, len(self.data)
|
||||
except (socket.error, IMAP4.error) as e:
|
||||
logger.error(f"Refresh failed, attempting to reconnect: {str(e)}")
|
||||
self.connect_imap()
|
||||
self.login_and_connect()
|
||||
self.mail_count = self._fetch_count()
|
||||
self.data = self._fetch_all()
|
||||
return self.data, self.mail_count, len(self.data)
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=2, exceptions=(socket.error, IMAP4.error))
|
||||
def _connect_inbox(self) -> None:
|
||||
"""
|
||||
Connect to INBOX with retry mechanism
|
||||
Raises: IMAP4.error: If connection to INBOX fails
|
||||
"""
|
||||
logger.info("Selecting INBOX folder")
|
||||
status, _ = self.mail.select("INBOX")
|
||||
if status != 'OK':
|
||||
error_msg = "Failed to connect to INBOX"
|
||||
logger.error(error_msg)
|
||||
raise IMAP4.error(error_msg)
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=2, exceptions=(socket.error, IMAP4.error))
|
||||
def _fetch_count(self) -> int:
|
||||
"""
|
||||
Fetch mail count with retry mechanism
|
||||
Returns: Number of emails
|
||||
Raises: IMAP4.error: If fetching mail count fails
|
||||
"""
|
||||
try:
|
||||
status, uids = self.mail.uid('SORT', '(REVERSE DATE)', 'UTF-8', 'ALL', 'FROM', f'"{self.config.MAILBOX}"')
|
||||
if status != 'OK':
|
||||
raise IMAP4.error("Failed to get mail count")
|
||||
count = len(uids[0].split()) if uids[0] else 0
|
||||
logger.info(f"Found {count} emails from {self.config.MAILBOX}")
|
||||
return count
|
||||
except (socket.error, IMAP4.error) as e:
|
||||
logger.error(f"Error fetching mail count: {str(e)}")
|
||||
raise
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=2, exceptions=(socket.error, IMAP4.error))
|
||||
def _fetch_all(self) -> List[Mails]:
|
||||
"""
|
||||
Fetch all mails with retry mechanism
|
||||
Returns: List of mail objects
|
||||
Raises: IMAP4.error: If fetching mails fails
|
||||
"""
|
||||
self.data = []
|
||||
try:
|
||||
status, uids = self.mail.uid('SORT', '(REVERSE DATE)', 'UTF-8', 'ALL', 'FROM', f'"{self.config.MAILBOX}"')
|
||||
if status != 'OK':
|
||||
raise IMAP4.error("Mail search failed")
|
||||
if not uids[0]:
|
||||
logger.info("No emails found matching criteria")
|
||||
return self.data
|
||||
uid_list = uids[0].split()
|
||||
logger.info(f"Processing {len(uid_list)} emails")
|
||||
for uid in uid_list:
|
||||
try:
|
||||
status, msg_data = self.mail.uid('fetch', uid, '(RFC822)')
|
||||
if status == 'OK' and msg_data[0] is not None:
|
||||
self.data.append(Mails(uid, msg_data[0][1]))
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch email with UID {uid}: {str(e)}")
|
||||
continue
|
||||
logger.info(f"Successfully fetched {len(self.data)} emails")
|
||||
return self.data
|
||||
except (socket.error, IMAP4.error) as e:
|
||||
logger.error(f"Error fetching emails: {str(e)}")
|
||||
raise
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=1, exceptions=(socket.error, IMAP4.error))
|
||||
def move_to_folder(self, uid: Union[str, bytes], folder: str):
|
||||
"""
|
||||
Move message to folder with retry mechanism
|
||||
Args: uid: Email UID, folder: Destination folder
|
||||
"""
|
||||
try:
|
||||
log_uid = uid
|
||||
if isinstance(uid, bytes):
|
||||
log_uid = uid.decode('utf-8', errors='replace')
|
||||
elif isinstance(uid, str):
|
||||
uid = uid.encode('utf-8')
|
||||
logger.info(f"Moving email {log_uid} to {folder} folder")
|
||||
self.mail.uid('MOVE', uid, folder)
|
||||
self.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to move email to folder: {str(e)}")
|
||||
return False
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=1, exceptions=(socket.error, IMAP4.error))
|
||||
def copy_to_folder(self, uid: Union[str, bytes], folder: str):
|
||||
"""
|
||||
Copy message to folder with retry mechanism
|
||||
Args: uid: Email UID, folder: Destination folder
|
||||
"""
|
||||
try:
|
||||
log_uid = uid
|
||||
if isinstance(uid, bytes):
|
||||
log_uid = uid.decode('utf-8', errors='replace')
|
||||
elif isinstance(uid, str):
|
||||
uid = uid.encode('utf-8')
|
||||
logger.info(f"Copying email {log_uid} to {folder} folder")
|
||||
self.mail.uid('COPY', uid, folder)
|
||||
self.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to copy email to folder: {str(e)}")
|
||||
return False
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=1, exceptions=(socket.error, IMAP4.error))
|
||||
def mark_no_attachment(self, uid: Union[str, bytes]):
|
||||
"""
|
||||
Move message to no attachment folder with retry mechanism
|
||||
Args: uid: Email UID
|
||||
"""
|
||||
self.move_to_folder(uid, self.config.NO_ATTACHMENT_FOLDER)
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=1, exceptions=(socket.error, IMAP4.error))
|
||||
def mark_completed(self, uid: Union[str, bytes]):
|
||||
"""
|
||||
Move message to completed folder with retry mechanism
|
||||
Args: uid: Email UID
|
||||
"""
|
||||
self.move_to_folder(uid, self.config.COMPLETED_FOLDER)
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=1, exceptions=(socket.error, IMAP4.error))
|
||||
def delete(self, uid):
|
||||
"""
|
||||
Delete message with retry mechanism
|
||||
Args: uid: Email UID
|
||||
"""
|
||||
try:
|
||||
log_uid = uid
|
||||
if isinstance(uid, bytes):
|
||||
log_uid = uid.decode('utf-8', errors='replace')
|
||||
logger.info(f"Marking email {log_uid} for deletion")
|
||||
self.mail.uid('STORE', uid, '+FLAGS', r'(\Deleted)')
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete email: {str(e)}")
|
||||
raise
|
||||
|
||||
@retry_on_connection_error(max_retries=2, delay=1, exceptions=(socket.error, IMAP4.error))
|
||||
def commit(self):
|
||||
"""
|
||||
Commit pending operations with retry mechanism
|
||||
Raises: Exception: If commit fails
|
||||
"""
|
||||
try:
|
||||
logger.info("Committing changes (expunge)")
|
||||
self.mail.expunge()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to commit changes: {str(e)}")
|
||||
raise
|
||||
|
||||
def logout(self):
|
||||
"""Logout from IMAP server"""
|
||||
if self.mail and self.is_connected:
|
||||
try:
|
||||
logger.info("Logging out from IMAP server")
|
||||
self.mail.close()
|
||||
self.mail.logout()
|
||||
self.is_connected = False
|
||||
except Exception as e:
|
||||
logger.warning(f"Logout failed: {str(e)}")
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
"""Get count of emails"""
|
||||
return len(self.data)
|
||||
@@ -1,29 +1,104 @@
|
||||
import os
|
||||
import uuid
|
||||
import asyncio
|
||||
from typing import List
|
||||
|
||||
from app.services.mail.IsBank.runner import initialize_service
|
||||
from app.services.common.service_base_async import ServiceBaseAsync
|
||||
|
||||
|
||||
PRODUCE_ENABLED = os.getenv("PRODUCE_ENABLED", "true").lower() == "true"
|
||||
PRODUCE_BATCH = int(os.getenv("PRODUCE_BATCH", "3")) # her produce tick'inde kaç iş
|
||||
TASK_TYPE = os.getenv("TASK_TYPE", "db-task") # iş tipi (task_id'de de kullanılır)
|
||||
CONSUME_SLEEP_SEC = float(os.getenv("CONSUME_SLEEP_SEC", "0.5")) # işleme süresi simülasyonu (sn)
|
||||
STATIC_IDS = ["2c47f1073a9d4f05aad6c15484894a74", "65827e3452b545d6845e050a503401f4", "5c663088f09d4062b4e567f47335fb1e"]
|
||||
from .mail_handler import Mails
|
||||
from .IsBank.params import IsBankConfig
|
||||
|
||||
|
||||
async def produce(service: ServiceBaseAsync):
|
||||
for biz_id in STATIC_IDS:
|
||||
deterministic_task_id = f"{TASK_TYPE}:{biz_id}"
|
||||
payload = {"id": biz_id, "op": "sync", "source": "db-service"}
|
||||
await service.enqueue(payload, TASK_TYPE, task_id=deterministic_task_id)
|
||||
print(f"[DB] produce tick attempted ids={','.join(STATIC_IDS)}")
|
||||
PRODUCE_BURST = int(os.getenv("PRODUCE_BURST", "10"))
|
||||
PRODUCE_ONCE = os.getenv("PRODUCE_ONCE", "true").lower() == "true"
|
||||
EVENT_TYPE = os.getenv("EVENT_TYPE", "db-event")
|
||||
|
||||
_produced = False
|
||||
PROCESS_SEC = 10
|
||||
email_service = initialize_service()
|
||||
|
||||
|
||||
async def consume(service: ServiceBaseAsync, job: dict):
|
||||
await asyncio.sleep(CONSUME_SLEEP_SEC)
|
||||
print(f"[DB] consumed task={job['task_id']} attempts={job.get('_attempts', 0)}")
|
||||
def generate_unique_with_mail_id(mail_id: str, service_prefix: str):
|
||||
return f"{service_prefix}_{mail_id}"
|
||||
|
||||
|
||||
def process_mail_with_attachments(mail: Mails, mail_id: str):
|
||||
"""
|
||||
Process an email with attachments using MailReaderService
|
||||
Args: mail: Mail object, mail_id: Mail ID
|
||||
Raises: Exception: If processing mail fails
|
||||
"""
|
||||
try:
|
||||
mail_to_dict = mail.to_dict()
|
||||
task_uuid = generate_unique_with_mail_id(mail_id, IsBankConfig.SERVICE_NAME)
|
||||
process_mail_dict = dict(mail_id=mail_id, mail_data=mail_to_dict, service_prefix=email_service.config.SERVICE_PREFIX)
|
||||
return task_uuid, process_mail_dict
|
||||
except Exception as e:
|
||||
print(f"Email Service Runner Error processing mail {mail_id}: {str(e)}")
|
||||
raise
|
||||
|
||||
|
||||
def drop():
|
||||
"""Clean up resources"""
|
||||
try:
|
||||
email_service.commit()
|
||||
except Exception as e:
|
||||
print(f"Error during commit on drop: {str(e)}")
|
||||
try:
|
||||
email_service.logout()
|
||||
except Exception as e:
|
||||
print(f"Error during logout on drop: {str(e)}")
|
||||
|
||||
|
||||
async def produce(svc: ServiceBaseAsync):
|
||||
mails, count, length = email_service.refresh()
|
||||
for mail in mails:
|
||||
if not getattr(mail, 'id', None):
|
||||
print("Skipping email with no ID")
|
||||
continue
|
||||
mail_id, mail_dict = mail.id.decode('utf-8'), mail.to_dict()
|
||||
try:
|
||||
if mail.attachments:
|
||||
if any([str(attachment['filename']).lower().endswith('.pdf') for attachment in mail_dict['attachments']]):
|
||||
email_service.mark_no_attachment(mail_id)
|
||||
else:
|
||||
task_uuid, process_mail_dict = process_mail_with_attachments(mail, mail_id)
|
||||
await svc.enqueue(task_id=task_uuid, payload=process_mail_dict, type_="mail.service.isbank")
|
||||
else:
|
||||
email_service.mark_no_attachment(mail_id)
|
||||
except Exception as e:
|
||||
print(f"Error processing email {mail_id}: {str(e)}")
|
||||
continue
|
||||
await asyncio.sleep(PROCESS_SEC)
|
||||
|
||||
|
||||
async def handle_from_parser(svc: ServiceBaseAsync, job):
|
||||
print("Mail Consumer from parser:", job)
|
||||
await asyncio.sleep(PROCESS_SEC)
|
||||
return
|
||||
|
||||
|
||||
async def handle_database_publish(svc: ServiceBaseAsync, job):
|
||||
await asyncio.sleep(PROCESS_SEC)
|
||||
print("Mail Consumer from database:", job)
|
||||
return
|
||||
|
||||
|
||||
async def handle_from_mail(svc: ServiceBaseAsync, job):
|
||||
await asyncio.sleep(PROCESS_SEC)
|
||||
print("Mail Consumer from mail:", job)
|
||||
return
|
||||
|
||||
|
||||
async def consume_default(svc, job):
|
||||
await asyncio.sleep(PROCESS_SEC)
|
||||
print("Mail Consumer default:", job)
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(ServiceBaseAsync(produce, consume).run())
|
||||
|
||||
svc = ServiceBaseAsync(produce, consume_default,
|
||||
handlers={"parser.publish": handle_from_parser, "mail.publish": handle_from_mail, "database.publish": handle_database_publish}
|
||||
)
|
||||
asyncio.run(svc.run())
|
||||
|
||||
@@ -11,9 +11,15 @@ requires-python = ">=3.11"
|
||||
authors = [
|
||||
{ name = "Berkay Karatay", email = "karatay.berkay@gmail.com" }
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"redis>=5.0.0",
|
||||
"aiosqlite>=0.19.0",
|
||||
"aio-pika>=9.4.1",
|
||||
"prometheus-client>=0.20.0",
|
||||
"uvloop>=0.19.0",
|
||||
"arrow>=1.3.0",
|
||||
"pydantic>=2.0.0",
|
||||
"pydantic-settings>=2.0.0",
|
||||
"email-validator>=2.0.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
Reference in New Issue
Block a user