199 lines
7.1 KiB
Python
199 lines
7.1 KiB
Python
from typing import Dict, Optional
|
|
from datetime import datetime
|
|
from common.log.module_logger import ModuleLogger
|
|
from backend.models.models import EmailBounceDoc, EmailSendStatusDoc
|
|
from common.constants.email import BounceType
|
|
|
|
|
|
class EmailBounceService:
|
|
def __init__(self):
|
|
self.module_logger = ModuleLogger(sender_id="EmailBounceService")
|
|
|
|
async def process_bounce_event(self, email: str, tenant_id: str, bounce_type: BounceType,
|
|
reason: str, message_id: str = None) -> Dict:
|
|
"""handle bounce event, create email_id association"""
|
|
try:
|
|
# 1. find corresponding email record
|
|
email_status_doc = await EmailSendStatusDoc.find_one(
|
|
EmailSendStatusDoc.recipient_email == email,
|
|
EmailSendStatusDoc.tenant_id == tenant_id
|
|
).sort(-EmailSendStatusDoc.created_at)
|
|
|
|
email_id = None
|
|
template_id = None
|
|
|
|
if email_status_doc:
|
|
email_id = email_status_doc.email_id
|
|
template_id = email_status_doc.template_id
|
|
|
|
# 2. update email status to bounced
|
|
email_status_doc.status = EmailSendStatus.BOUNCED
|
|
email_status_doc.updated_at = datetime.utcnow()
|
|
await email_status_doc.save()
|
|
|
|
await self.module_logger.log_info(
|
|
"Found email record for bounce",
|
|
properties={
|
|
"email": email,
|
|
"email_id": email_id,
|
|
"tenant_id": tenant_id
|
|
}
|
|
)
|
|
else:
|
|
await self.module_logger.log_warning(
|
|
"No email record found for bounce",
|
|
properties={
|
|
"email": email,
|
|
"tenant_id": tenant_id
|
|
}
|
|
)
|
|
|
|
# 3. create bounce record
|
|
bounce_doc = EmailBounceDoc(
|
|
email=email,
|
|
tenant_id=tenant_id,
|
|
email_id=email_id,
|
|
template_id=template_id,
|
|
bounce_type=bounce_type,
|
|
reason=reason,
|
|
bounced_at=datetime.utcnow(),
|
|
original_message_id=message_id,
|
|
processed=False
|
|
)
|
|
|
|
await bounce_doc.save()
|
|
|
|
await self.module_logger.log_info(
|
|
"Bounce event processed successfully",
|
|
properties={
|
|
"email": email,
|
|
"email_id": email_id,
|
|
"tenant_id": tenant_id,
|
|
"bounce_type": bounce_type.value,
|
|
"reason": reason
|
|
}
|
|
)
|
|
|
|
return {
|
|
"email": email,
|
|
"email_id": email_id,
|
|
"tenant_id": tenant_id,
|
|
"bounce_type": bounce_type.value,
|
|
"reason": reason,
|
|
"processed": True
|
|
}
|
|
|
|
except Exception as e:
|
|
await self.module_logger.log_error(
|
|
"Failed to process bounce event",
|
|
properties={
|
|
"email": email,
|
|
"tenant_id": tenant_id,
|
|
"error": str(e)
|
|
}
|
|
)
|
|
raise
|
|
|
|
async def get_bounce_info(self, email: str, tenant_id: str) -> Optional[Dict]:
|
|
"""get bounce info"""
|
|
try:
|
|
bounce_doc = await EmailBounceDoc.find_one(
|
|
EmailBounceDoc.email == email,
|
|
EmailBounceDoc.tenant_id == tenant_id
|
|
).sort(-EmailBounceDoc.created_at)
|
|
|
|
if not bounce_doc:
|
|
return None
|
|
|
|
return {
|
|
"email": bounce_doc.email,
|
|
"email_id": bounce_doc.email_id,
|
|
"tenant_id": bounce_doc.tenant_id,
|
|
"template_id": bounce_doc.template_id,
|
|
"bounce_type": bounce_doc.bounce_type.value,
|
|
"reason": bounce_doc.reason,
|
|
"bounced_at": bounce_doc.bounced_at.isoformat(),
|
|
"original_message_id": bounce_doc.original_message_id,
|
|
"processed": bounce_doc.processed,
|
|
"processed_at": bounce_doc.processed_at.isoformat() if bounce_doc.processed_at else None,
|
|
"created_at": bounce_doc.created_at.isoformat()
|
|
}
|
|
|
|
except Exception as e:
|
|
await self.module_logger.log_error(
|
|
"Failed to get bounce info",
|
|
properties={
|
|
"email": email,
|
|
"tenant_id": tenant_id,
|
|
"error": str(e)
|
|
}
|
|
)
|
|
raise
|
|
|
|
async def mark_bounce_processed(self, email: str, tenant_id: str) -> bool:
|
|
"""mark bounce as processed"""
|
|
try:
|
|
bounce_doc = await EmailBounceDoc.find_one(
|
|
EmailBounceDoc.email == email,
|
|
EmailBounceDoc.tenant_id == tenant_id
|
|
).sort(-EmailBounceDoc.created_at)
|
|
|
|
if bounce_doc:
|
|
bounce_doc.processed = True
|
|
bounce_doc.processed_at = datetime.utcnow()
|
|
await bounce_doc.save()
|
|
|
|
await self.module_logger.log_info(
|
|
"Bounce marked as processed",
|
|
properties={
|
|
"email": email,
|
|
"tenant_id": tenant_id
|
|
}
|
|
)
|
|
return True
|
|
|
|
return False
|
|
|
|
except Exception as e:
|
|
await self.module_logger.log_error(
|
|
"Failed to mark bounce as processed",
|
|
properties={
|
|
"email": email,
|
|
"tenant_id": tenant_id,
|
|
"error": str(e)
|
|
}
|
|
)
|
|
raise
|
|
|
|
async def is_blacklisted(self, email: str, tenant_id: str):
|
|
"""check if email is blacklisted"""
|
|
try:
|
|
bounce_doc = await EmailBounceDoc.find_one(
|
|
EmailBounceDoc.email == email,
|
|
EmailBounceDoc.tenant_id == tenant_id,
|
|
EmailBounceDoc.bounce_type == BounceType.HARD_BOUNCE
|
|
)
|
|
|
|
is_blacklisted = bounce_doc is not None
|
|
|
|
await self.module_logger.log_info(
|
|
"Email blacklist check completed",
|
|
properties={
|
|
"email": email,
|
|
"tenant_id": tenant_id,
|
|
"is_blacklisted": is_blacklisted
|
|
}
|
|
)
|
|
|
|
return is_blacklisted
|
|
|
|
except Exception as e:
|
|
await self.module_logger.log_error(
|
|
"Failed to check email blacklist",
|
|
properties={
|
|
"email": email,
|
|
"tenant_id": tenant_id,
|
|
"error": str(e)
|
|
}
|
|
)
|
|
return False |