refactor(email_sender): change email_sender type from list to str

This commit is contained in:
YuehuCao 2025-08-14 21:08:43 +08:00
parent b6d5ae97ee
commit 370cd61fd2
12 changed files with 248 additions and 398 deletions

View File

@ -6,49 +6,32 @@ class EmailSenderHub:
def __init__(self): def __init__(self):
self.email_sender_manager = EmailSenderManager() self.email_sender_manager = EmailSenderManager()
async def get_email_senders(self, tenant_id: str): async def get_email_sender(self, tenant_id: str):
"""get email senders for tenant""" """get email senders for tenant"""
if not tenant_id: if not tenant_id:
raise ValueError("tenant_id is required") raise ValueError("tenant_id is required")
return await self.email_sender_manager.get_email_senders(tenant_id) return await self.email_sender_manager.get_email_sender(tenant_id)
async def set_email_senders(self, tenant_id: str, email_senders: List[str]): async def set_email_sender(self, tenant_id: str, email_sender: str):
"""set email senders for tenant""" """set email sender for tenant"""
if not tenant_id: if not tenant_id:
raise ValueError("tenant_id is required") raise ValueError("tenant_id is required")
if not email_senders or not isinstance(email_senders, list): if not email_sender:
raise ValueError("email_senders must be a non-empty list") raise ValueError("email_sender must be provided")
return await self.email_sender_manager.set_email_senders(tenant_id, email_senders) return await self.email_sender_manager.set_email_sender(tenant_id, email_sender)
async def add_email_senders(self, tenant_id: str, new_senders: List[str]): async def update_email_sender(self, tenant_id: str, email_sender: str):
"""add email senders to tenant""" """update email sender for tenant"""
if not tenant_id: if not tenant_id:
raise ValueError("tenant_id is required") raise ValueError("tenant_id is required")
if not new_senders or not isinstance(new_senders, list): if not email_sender:
raise ValueError("new_senders must be a non-empty list") raise ValueError("email_sender must be provided")
return await self.email_sender_manager.add_email_senders(tenant_id, new_senders) return await self.email_sender_manager.update_email_sender(tenant_id, email_sender)
async def remove_email_senders(self, tenant_id: str, emails_to_remove: List[str]):
"""remove email senders from tenant"""
if not tenant_id:
raise ValueError("tenant_id is required")
if not emails_to_remove or not isinstance(emails_to_remove, list):
raise ValueError("emails_to_remove must be a non-empty list")
return await self.email_sender_manager.remove_email_senders(tenant_id, emails_to_remove)
async def clear_email_senders(self, tenant_id: str):
"""clear email senders for tenant"""
if not tenant_id:
raise ValueError("tenant_id is required")
return await self.email_sender_manager.clear_email_senders(tenant_id)
async def delete_email_sender(self, tenant_id: str): async def delete_email_sender(self, tenant_id: str):
"""delete email sender for tenant""" """delete email sender for tenant"""

View File

@ -24,11 +24,11 @@ class TenantNotificationHub:
region: int, region: int,
subject_properties: Dict = {}, subject_properties: Dict = {},
body_properties: Dict = {}, body_properties: Dict = {},
sender_emails: Optional[List[str]] = None, sender_email: str = None,
priority: str = "normal", priority: str = "normal",
tracking_enabled: bool = True tracking_enabled: bool = True
): ):
"""Send email using tenant's template and email senders""" """Send email using tenant's template and email sender"""
try: try:
# 1. check if tenant has access to template # 1. check if tenant has access to template
await self.template_message_hub.verify_tenant_access(template_id, tenant_id, region) await self.template_message_hub.verify_tenant_access(template_id, tenant_id, region)
@ -41,12 +41,12 @@ class TenantNotificationHub:
region=region region=region
) )
# 3. get tenant email senders # 3. get tenant email sender
default_sender_email = self.notification_constants.DEFAULT_EMAIL_SENDER default_sender_email = self.notification_constants.DEFAULT_EMAIL_SENDER
if sender_emails is None: if sender_email is None:
tenant_email_senders = await self.email_sender_hub.get_email_senders(tenant_id) tenant_email_sender = await self.email_sender_hub.get_email_sender(tenant_id)
if not tenant_email_senders: if not tenant_email_sender:
sender_emails = [default_sender_email] sender_email = default_sender_email
await self.module_logger.log_info( await self.module_logger.log_info(
"Using default email sender for tenant", "Using default email sender for tenant",
properties={ properties={
@ -55,22 +55,15 @@ class TenantNotificationHub:
} }
) )
else: else:
sender_emails = tenant_email_senders sender_email = tenant_email_sender
# 4. check if sender_emails are valid # 4. call TenantNotificationManager to send email
if sender_emails != [default_sender_email]:
tenant_senders = await self.email_sender_hub.get_email_senders(tenant_id)
invalid_senders = [email for email in sender_emails if email not in tenant_senders]
if invalid_senders:
raise InvalidDataError(f"Invalid email senders for tenant: {invalid_senders}")
# 5. call TenantNotificationManager to send email
result = await self.tenant_notification_manager.send_tenant_email( result = await self.tenant_notification_manager.send_tenant_email(
tenant_id=tenant_id, tenant_id=tenant_id,
template_id=template_id, template_id=template_id,
rendered_template=rendered_template, rendered_template=rendered_template,
recipient_emails=recipient_emails, recipient_emails=recipient_emails,
sender_emails=sender_emails, sender_email=sender_email,
region=region, region=region,
priority=priority, priority=priority,
tracking_enabled=tracking_enabled tracking_enabled=tracking_enabled
@ -82,7 +75,7 @@ class TenantNotificationHub:
"tenant_id": tenant_id, "tenant_id": tenant_id,
"template_id": template_id, "template_id": template_id,
"recipient_count": len(recipient_emails), "recipient_count": len(recipient_emails),
"sender_count": len(sender_emails), "sender_email": sender_email,
"message_id": result.get("message_id") "message_id": result.get("message_id")
} }
) )

View File

@ -10,90 +10,49 @@ class EmailSenderManager:
self.email_sender_service = EmailSenderService() self.email_sender_service = EmailSenderService()
self.module_logger = ModuleLogger(sender_id="EmailSenderManager") self.module_logger = ModuleLogger(sender_id="EmailSenderManager")
async def get_email_senders(self, tenant_id: str): async def get_email_sender(self, tenant_id: str):
"""get email sendersfor tenant""" """get email sendersfor tenant"""
email_senders = await self.email_sender_service.get_email_senders(tenant_id) email_sender = await self.email_sender_service.get_email_sender(tenant_id)
await self.module_logger.log_info( await self.module_logger.log_info(
info="Email senders retrieved", info="Email sender retrieved",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"sender_count": len(email_senders) "email_sender": email_sender
} }
) )
return email_senders return email_sender
async def set_email_senders(self, tenant_id: str, email_senders: List[str]): async def set_email_sender(self, tenant_id: str, email_sender: str):
"""set email senders for tenant""" """set email sender for tenant"""
if not email_senders: if not email_sender:
raise ValueError("Email senders list cannot be empty") raise ValueError("Email sender must be provided")
for email in email_senders: result = await self.email_sender_service.set_email_sender(tenant_id, email_sender)
if not self._is_valid_email(email):
raise ValueError(f"Invalid email format: {email}")
result = await self.email_sender_service.set_email_senders(tenant_id, email_senders)
await self.module_logger.log_info( await self.module_logger.log_info(
info="Email senders set", info="Email senders set",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"sender_count": len(email_senders) "email_sender": email_sender
} }
) )
return result return result
async def add_email_senders(self, tenant_id: str, new_senders: List[str]): async def update_email_sender(self, tenant_id: str, email_sender: str):
"""add email senders to tenant""" """update email sender for tenant"""
if not new_senders: if not email_sender:
raise ValueError("New senders list cannot be empty") raise ValueError("Email sender must be provided")
for email in new_senders: result = await self.email_sender_service.update_email_sender(tenant_id, email_sender)
if not self._is_valid_email(email):
raise ValueError(f"Invalid email format: {email}")
result = await self.email_sender_service.add_email_senders(tenant_id, new_senders)
await self.module_logger.log_info( await self.module_logger.log_info(
info="Email senders added", info="Email senders set",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"new_sender_count": len(new_senders), "email_sender": email_sender
"success": result.get("success", False)
}
)
return result
async def remove_email_senders(self, tenant_id: str, emails_to_remove: List[str]):
"""remove email senders from tenant"""
if not emails_to_remove:
raise ValueError("Emails to remove list cannot be empty")
result = await self.email_sender_service.remove_email_senders(tenant_id, emails_to_remove)
await self.module_logger.log_info(
info="Email senders removed",
properties={
"tenant_id": tenant_id,
"removed_count": len(emails_to_remove),
"success": result.get("success", False)
}
)
return result
async def clear_email_senders(self, tenant_id: str):
"""clear email senders for tenant"""
result = await self.email_sender_service.clear_email_senders(tenant_id)
await self.module_logger.log_info(
info="Email senders cleared",
properties={
"tenant_id": tenant_id,
"success": result.get("success", False)
} }
) )
@ -113,12 +72,7 @@ class EmailSenderManager:
return result return result
def _is_valid_email(self, email: str) -> bool:
"""validate email format"""
# TODO: add more complex email format validation if needed
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None

View File

@ -28,22 +28,22 @@ class TenantNotificationManager:
template_id: str, template_id: str,
rendered_template: Dict, rendered_template: Dict,
recipient_emails: List[str], recipient_emails: List[str],
sender_emails: List[str], sender_email: str,
region: int, region: int,
priority: str = "normal", priority: str = "normal",
tracking_enabled: bool = True tracking_enabled: bool = True
): ):
"""Send tenant email using existing EMAIL queue with validation and protection""" """Send tenant email using existing EMAIL with validation and protection"""
try: try:
# 1. validate recipient emails # 1. validate recipient emails
valid_recipients, invalid_recipients = await self.email_validation_service.validate_emails(recipient_emails) valid_recipients, invalid_recipients = await self.email_validation_service.validate_emails(recipient_emails)
valid_senders, invalid_senders = await self.email_validation_service.validate_sender_emails(tenant_id, sender_emails) valid_sender = await self.email_validation_service.validate_sender_email(tenant_id, sender_email)
if not valid_recipients: if not valid_recipients:
raise InvalidDataError("No valid recipient emails found") raise InvalidDataError("No valid recipient emails found")
if not valid_senders: if valid_sender is None:
raise InvalidDataError("No valid sender emails found") raise InvalidDataError("Invalid sender email")
# 2. check blacklisted recipients # 2. check blacklisted recipients
blacklisted_recipients = [] blacklisted_recipients = []
@ -58,7 +58,7 @@ class TenantNotificationManager:
raise InvalidDataError("All recipient emails are blacklisted") raise InvalidDataError("All recipient emails are blacklisted")
# 3. check rate limit # 3. check rate limit
rate_limit_result = await self.email_spam_protection_service.check_rate_limit(tenant_id, valid_senders[0]) rate_limit_result = await self.email_spam_protection_service.check_rate_limit(tenant_id, sender_email)
if not rate_limit_result["allowed"]: if not rate_limit_result["allowed"]:
raise InvalidDataError("Rate limit exceeded") raise InvalidDataError("Rate limit exceeded")
@ -76,7 +76,7 @@ class TenantNotificationManager:
"tenant_id": tenant_id, "tenant_id": tenant_id,
"template_id": template_id, "template_id": template_id,
"destination_emails": valid_recipients, "destination_emails": valid_recipients,
"sender_emails": valid_senders, "sender_email": valid_sender,
"subject_properties": rendered_template.get("subject_properties", {}), "subject_properties": rendered_template.get("subject_properties", {}),
"body_properties": rendered_template.get("body_properties", {}), "body_properties": rendered_template.get("body_properties", {}),
"region": region, "region": region,
@ -87,7 +87,7 @@ class TenantNotificationManager:
"receiver_type": "email", "receiver_type": "email",
"validation_info": { "validation_info": {
"invalid_recipients": invalid_recipients, "invalid_recipients": invalid_recipients,
"invalid_senders": invalid_senders, "invalid_sender": not valid_sender,
"blacklisted_recipients": blacklisted_recipients, "blacklisted_recipients": blacklisted_recipients,
"rate_limit_info": rate_limit_result, "rate_limit_info": rate_limit_result,
"spam_detection_info": spam_result "spam_detection_info": spam_result
@ -101,7 +101,7 @@ class TenantNotificationManager:
"tenant_id": tenant_id, "tenant_id": tenant_id,
"template_id": template_id, "template_id": template_id,
"destination_emails": [recipient_email], "destination_emails": [recipient_email],
"sender_emails": valid_senders, "sender_email": valid_sender,
"subject_properties": rendered_template.get("subject_properties", {}), "subject_properties": rendered_template.get("subject_properties", {}),
"body_properties": rendered_template.get("body_properties", {}), "body_properties": rendered_template.get("body_properties", {}),
"region": region, "region": region,
@ -112,7 +112,7 @@ class TenantNotificationManager:
"receiver_type": "email", "receiver_type": "email",
"validation_info": { "validation_info": {
"invalid_recipients": invalid_recipients, "invalid_recipients": invalid_recipients,
"invalid_senders": invalid_senders, "invalid_sender": not valid_sender,
"blacklisted_recipients": blacklisted_recipients, "blacklisted_recipients": blacklisted_recipients,
"rate_limit_info": rate_limit_result, "rate_limit_info": rate_limit_result,
"spam_detection_info": spam_result "spam_detection_info": spam_result
@ -138,8 +138,8 @@ class TenantNotificationManager:
"valid_recipient_count": len(valid_recipients), "valid_recipient_count": len(valid_recipients),
"invalid_recipient_count": len(invalid_recipients), "invalid_recipient_count": len(invalid_recipients),
"blacklisted_recipient_count": len(blacklisted_recipients), "blacklisted_recipient_count": len(blacklisted_recipients),
"valid_sender_count": len(valid_senders), "sender_email": sender_email,
"invalid_sender_count": len(invalid_senders) "sender_valid": valid_sender
} }
) )
@ -154,9 +154,8 @@ class TenantNotificationManager:
"valid_recipients": len(valid_recipients), "valid_recipients": len(valid_recipients),
"invalid_recipients": len(invalid_recipients), "invalid_recipients": len(invalid_recipients),
"blacklisted_recipients": len(blacklisted_recipients), "blacklisted_recipients": len(blacklisted_recipients),
"total_senders": len(sender_emails), "sender_email": sender_email,
"valid_senders": len(valid_senders), "sender_valid": valid_sender
"invalid_senders": len(invalid_senders)
} }
} }

View File

@ -49,17 +49,18 @@ class EmailHandler:
tenant_id: str, tenant_id: str,
template_id: str, template_id: str,
recipient_email: str, recipient_email: str,
sender_emails: List[str], sender_email: str,
subject_properties: Dict = {}, subject_properties: Dict = {},
body_properties: Dict = {} body_properties: Dict = {},
tracking_enabled: bool = True
): ):
"""Send tenant email using specified senders""" """Send tenant email using specified sender"""
module_logger = ModuleLogger(sender_id="EmailHandler") module_logger = ModuleLogger(sender_id="EmailHandler")
try: try:
email_id = str(uuid.uuid4()) email_id = str(uuid.uuid4())
from_email = sender_emails[0] if sender_emails else app_settings.EMAIL_FROM from_email = sender_email if sender_email else app_settings.EMAIL_FROM
subject = subject_properties.get("subject", "No Subject") subject = subject_properties.get("subject", "No Subject")
html_content = body_properties.get("html_content", "") html_content = body_properties.get("html_content", "")
@ -68,7 +69,7 @@ class EmailHandler:
email_status_doc = EmailSendStatusDoc( email_status_doc = EmailSendStatusDoc(
email_id=email_id, email_id=email_id,
tenant_id=tenant_id, tenant_id=tenant_id,
email_senders=sender_emails, email_sender=sender_email,
recipient_email=recipient_email, recipient_email=recipient_email,
template_id=template_id, template_id=template_id,
subject=subject, subject=subject,
@ -77,6 +78,20 @@ class EmailHandler:
) )
await email_status_doc.save() await email_status_doc.save()
# Create EmailTrackingDoc if tracking is enabled
tracking_doc = None
if tracking_enabled:
from backend.models.models import EmailTrackingDoc
tracking_doc = EmailTrackingDoc(
email_id=email_id,
tenant_id=tenant_id,
recipient_email=recipient_email,
template_id=template_id,
sent_at=datetime.utcnow(),
tracking_enabled=True
)
await tracking_doc.save()
mail = Mail( mail = Mail(
from_email=from_email, from_email=from_email,
to_emails=recipient_email, to_emails=recipient_email,
@ -84,6 +99,17 @@ class EmailHandler:
html_content=html_content, html_content=html_content,
) )
# Enable SendGrid tracking if tracking is enabled
if tracking_enabled:
from sendgrid.helpers.mail import TrackingSettings, ClickTracking, OpenTracking
tracking_settings = TrackingSettings()
click_tracking = ClickTracking(True, True) # Enable click tracking
open_tracking = OpenTracking(True) # Enable open tracking
tracking_settings.click_tracking = click_tracking
tracking_settings.open_tracking = open_tracking
mail.tracking_settings = tracking_settings
sg = SendGridAPIClient(app_settings.SENDGRID_API_KEY) sg = SendGridAPIClient(app_settings.SENDGRID_API_KEY)
response = sg.send(mail) response = sg.send(mail)
@ -92,6 +118,11 @@ class EmailHandler:
email_status_doc.message_id = str(response.headers.get('X-Message-Id', '')) email_status_doc.message_id = str(response.headers.get('X-Message-Id', ''))
await email_status_doc.save() await email_status_doc.save()
# Update tracking document with message_id
if tracking_doc:
tracking_doc.message_id = email_status_doc.message_id
await tracking_doc.save()
await module_logger.log_info( await module_logger.log_info(
f"Tenant email sent successfully", f"Tenant email sent successfully",
properties={ properties={

View File

@ -7,46 +7,46 @@ class EmailSenderHandler:
def __init__(self): def __init__(self):
self.module_logger = ModuleLogger(sender_id="EmailSenderHandler") self.module_logger = ModuleLogger(sender_id="EmailSenderHandler")
async def get_email_senders(self, tenant_id: str) -> List[str]: async def get_email_sender(self, tenant_id: str) -> str:
"""get email senders for tenant""" """get email sender for tenant"""
try: try:
doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True})
return doc.email_senders if doc else [] return doc.email_sender if doc else None
except Exception as e: except Exception as e:
await self.module_logger.log_error( await self.module_logger.log_error(
error="Failed to get email senders", error="Failed to get email sender",
properties={"tenant_id": tenant_id, "error": str(e)} properties={"tenant_id": tenant_id, "error": str(e)}
) )
return [] return None
async def set_email_senders(self, tenant_id: str, email_senders: List[str]): async def set_email_sender(self, tenant_id: str, email_sender: str):
"""set email senders for tenant""" """set email sender for tenant"""
try: try:
doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id}) doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id})
if doc: if doc:
await doc.set({"email_senders": email_senders}) await doc.set({"email_sender": email_sender})
await self.module_logger.log_info( await self.module_logger.log_info(
info="Email senders set in database", info="Email senders set in database",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"sender_count": len(email_senders) "email_sender": email_sender
} }
) )
return {"success": True, "email_senders": email_senders} return {"success": True, "email_sender": email_sender}
else: else:
doc = EmailSenderDoc(tenant_id=tenant_id, email_senders=email_senders) doc = EmailSenderDoc(tenant_id=tenant_id, email_sender=email_sender)
await doc.create() await doc.create()
await self.module_logger.log_info( await self.module_logger.log_info(
info="Email sender doc created with senders", info="Email sender doc created",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"sender_count": len(email_senders) "email_sender": email_sender
} }
) )
return {"success": True, "email_senders": doc.email_senders} return {"success": True, "email_sender": doc.email_sender}
except Exception as e: except Exception as e:
await self.module_logger.log_error( await self.module_logger.log_error(
error="Failed to set email senders", error="Failed to set email sender",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"error": str(e) "error": str(e)
@ -54,44 +54,25 @@ class EmailSenderHandler:
) )
raise raise
async def add_email_senders(self, tenant_id: str, new_senders: List[str]): async def update_email_sender(self, tenant_id: str, email_sender: str):
"""add email senders to tenant""" """update email sender for tenant (only if exists)"""
try: try:
if not new_senders or not isinstance(new_senders, list):
return {"success": False, "msg": "No sender provided"}
doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True})
if doc: if not doc:
original_set = set(doc.email_senders) raise ValueError("Email sender configuration not found")
new_set = set(new_senders)
to_add = new_set - original_set await doc.set({"email_sender": email_sender})
if not to_add:
return {"success": False, "msg": "All senders already exist"}
updated_list = list(original_set | new_set)
await doc.set({"email_senders": updated_list})
await self.module_logger.log_info( await self.module_logger.log_info(
info="Email senders added to database", info="Email sender updated in database",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"added_count": len(to_add), "email_sender": email_sender
"total_count": len(updated_list)
} }
) )
return {"success": True, "email_senders": updated_list} return {"success": True, "email_sender": email_sender}
else:
doc = EmailSenderDoc(tenant_id=tenant_id, email_senders=new_senders)
await doc.create()
await self.module_logger.log_info(
info="Email sender doc created with new senders",
properties={
"tenant_id": tenant_id,
"sender_count": len(new_senders)
}
)
return {"success": True, "email_senders": doc.email_senders}
except Exception as e: except Exception as e:
await self.module_logger.log_error( await self.module_logger.log_error(
error="Failed to add email senders", error="Failed to update email sender",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"error": str(e) "error": str(e)
@ -99,53 +80,27 @@ class EmailSenderHandler:
) )
raise raise
async def remove_email_senders(self, tenant_id: str, emails_to_remove: List[str]): async def remove_email_sender(self, tenant_id: str):
"""remove email senders from tenant""" """remove email sender from tenant"""
try: try:
doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True})
if not doc or not doc.email_senders: if not doc or not doc.email_sender:
return {"success": False, "msg": "No sender found"} return {"success": False, "msg": "No sender found"}
original_count = len(doc.email_senders) original_email_sender = doc.email_sender
doc.email_senders = [s for s in doc.email_senders if s not in emails_to_remove]
if len(doc.email_senders) == original_count:
return {"success": False, "msg": "No sender matched for removal"}
await doc.set({"email_senders": doc.email_senders}) await doc.set({"email_sender": None})
await self.module_logger.log_info( await self.module_logger.log_info(
info="Email senders removed from database", info="Email sender removed from database",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"removed_count": original_count - len(doc.email_senders), "removed_email_sender": original_email_sender
"remaining_count": len(doc.email_senders)
} }
) )
return {"success": True, "remaining": doc.email_senders} return {"success": True, "removed_email_sender": original_email_sender}
except Exception as e: except Exception as e:
await self.module_logger.log_error( await self.module_logger.log_error(
error="Failed to remove email senders", error="Failed to remove email sender",
properties={
"tenant_id": tenant_id,
"error": str(e)
}
)
raise
async def clear_email_senders(self, tenant_id: str):
"""clear up email senders for tenant"""
try:
doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True})
if doc:
await doc.set({"email_senders": []})
await self.module_logger.log_info(
info="Email senders cleared from database",
properties={"tenant_id": tenant_id}
)
return {"success": True}
return {"success": False, "msg": "No sender config found"}
except Exception as e:
await self.module_logger.log_error(
error="Failed to clear email senders",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"error": str(e) "error": str(e)

View File

@ -26,7 +26,7 @@ class MessageTemplateDoc(Document):
class EmailSenderDoc(Document): class EmailSenderDoc(Document):
tenant_id: str tenant_id: str
email_senders: List[str] = [] email_sender: Optional[str] = None
is_active: bool = True is_active: bool = True
class Settings: class Settings:
@ -36,7 +36,7 @@ class EmailSenderDoc(Document):
class EmailSendStatusDoc(Document): class EmailSendStatusDoc(Document):
email_id: str email_id: str
tenant_id: str tenant_id: str
email_senders: List[str] email_sender: Optional[str] = None
recipient_email: str recipient_email: str
template_id: Optional[str] = None template_id: Optional[str] = None
subject: str subject: str

View File

@ -45,43 +45,30 @@ class EmailValidationService:
) )
raise raise
async def validate_sender_emails(self, tenant_id: str, sender_emails: List[str]): async def validate_sender_email(self, tenant_id: str, sender_email: str):
"""validate sender emails, including format validation and permission validation""" """validate sender email, including format validation and permission validation"""
try: try:
valid_senders = [] authorized_sender = await self.email_sender_handler.get_email_sender(tenant_id)
invalid_senders = []
authorized_senders = await self.email_sender_handler.get_email_senders(tenant_id)
for sender_email in sender_emails:
# format validation # format validation
if not await self.email_validation_handler.is_valid_email(sender_email): if not await self.email_validation_handler.is_valid_email(sender_email):
invalid_senders.append(sender_email) return None
continue
# domain validation
if not await self.email_validation_handler.is_valid_domain(sender_email):
invalid_senders.append(sender_email)
continue
# sender permission validation # sender permission validation
# Allow support@freeleaps.com as default sender even if not in authorized_senders # Allow support@freeleaps.com as default sender even if not in authorized_senders
if sender_email not in authorized_senders and sender_email != "support@freeleaps.com": if sender_email not in ["support@freeleaps.com", authorized_sender]:
invalid_senders.append(sender_email) return None
continue
valid_senders.append(sender_email)
await self.module_logger.log_info( await self.module_logger.log_info(
"Sender email validation completed", "Sender email validation completed",
properties={ properties={
"tenant_id": tenant_id, "tenant_id": tenant_id,
"total_senders": len(sender_emails), "sender_email": sender_email,
"valid_count": len(valid_senders), "authorized_sender": authorized_sender,
"invalid_count": len(invalid_senders) "is_valid": True
} }
) )
return valid_senders, invalid_senders return sender_email
except Exception as e: except Exception as e:
await self.module_logger.log_error( await self.module_logger.log_error(

View File

@ -1,55 +1,55 @@
from typing import List from typing import List
from backend.infra.email_sender_handler import EmailSenderHandler from backend.infra.email_sender_handler import EmailSenderHandler
from backend.infra.email.email_validation_handler import EmailValidationHandler
from backend.models.models import EmailSenderDoc from backend.models.models import EmailSenderDoc
class EmailSenderService: class EmailSenderService:
def __init__(self): def __init__(self):
self.email_sender_handler = EmailSenderHandler() self.email_sender_handler = EmailSenderHandler()
self.email_validation_handler = EmailValidationHandler()
async def get_email_senders(self, tenant_id: str) -> List[str]: async def get_email_sender(self, tenant_id: str) -> str:
"""get email senders for tenant""" """get email sender for tenant"""
if not tenant_id: if not tenant_id:
raise ValueError("tenant_id is required") raise ValueError("tenant_id is required")
return await self.email_sender_handler.get_email_senders(tenant_id) return await self.email_sender_handler.get_email_sender(tenant_id)
async def set_email_senders(self, tenant_id: str, email_senders: List[str]): async def set_email_sender(self, tenant_id: str, email_sender: str):
"""set email senders for tenant""" """set email sender for tenant"""
if not tenant_id: if not tenant_id:
raise ValueError("tenant_id is required") raise ValueError("tenant_id is required")
if not email_senders or not isinstance(email_senders, list): if not email_sender:
raise ValueError("email_senders must be a non-empty list") raise ValueError("email_sender must be provided")
return await self.email_sender_handler.set_email_senders(tenant_id, email_senders) if not await self.email_validation_handler.is_valid_email(email_sender):
raise ValueError("Invalid email format")
async def add_email_senders(self, tenant_id: str, new_senders: List[str]): # TODO: check if the email is already registered in SendGrid or other email service provider
"""add email senders to tenant"""
return await self.email_sender_handler.set_email_sender(tenant_id, email_sender)
async def update_email_sender(self, tenant_id: str, email_sender: str):
"""update email sender for tenant (only if exists)"""
if not tenant_id: if not tenant_id:
raise ValueError("tenant_id is required") raise ValueError("tenant_id is required")
if not new_senders or not isinstance(new_senders, list): if not email_sender:
return {"success": False, "msg": "No sender provided"} raise ValueError("email_sender must be provided")
return await self.email_sender_handler.add_email_senders(tenant_id, new_senders) if not await self.email_validation_handler.is_valid_email(email_sender):
raise ValueError("Invalid email format")
async def remove_email_senders(self, tenant_id: str, emails_to_remove: List[str]): # Check if email sender exists first
"""remove email senders from tenant""" existing_sender = await self.email_sender_handler.get_email_sender(tenant_id)
if not tenant_id: if not existing_sender:
raise ValueError("tenant_id is required") raise ValueError("Email sender configuration not found for this tenant")
if not emails_to_remove or not isinstance(emails_to_remove, list): # TODO: check if the email is already registered in SendGrid or other email service provider
raise ValueError("emails_to_remove must be a non-empty list") # Only update if exists
return await self.email_sender_handler.update_email_sender(tenant_id, email_sender)
return await self.email_sender_handler.remove_email_senders(tenant_id, emails_to_remove)
async def clear_email_senders(self, tenant_id: str):
"""clear email senders for tenant"""
if not tenant_id:
raise ValueError("tenant_id is required")
return await self.email_sender_handler.clear_email_senders(tenant_id)
async def delete_email_sender(self, tenant_id: str): async def delete_email_sender(self, tenant_id: str):
"""delete email sender for tenant""" """delete email sender for tenant"""

View File

@ -16,14 +16,8 @@ token_manager = TokenManager()
email_sender_hub = EmailSenderHub() email_sender_hub = EmailSenderHub()
# Define the request body schema # Define the request body schema
class EmailSenderSetRequest(BaseModel): class EmailSenderRequest(BaseModel):
email_senders: List[str] email_sender: str
class EmailSenderAddRequest(BaseModel):
new_senders: List[str]
class EmailSenderRemoveRequest(BaseModel):
emails_to_remove: List[str]
# check credentials for admin and tenant # check credentials for admin and tenant
def admin_only(credentials: HTTPAuthorizationCredentials = Depends(security)): def admin_only(credentials: HTTPAuthorizationCredentials = Depends(security)):
@ -74,44 +68,45 @@ def tenant_only(credentials: HTTPAuthorizationCredentials = Depends(security)):
raise HTTPException(status_code=401, detail="Invalid token") raise HTTPException(status_code=401, detail="Invalid token")
# Web API # Web API
# Get email senders for tenant # Get email sender for tenant
@router.get( @router.get(
"/email_senders/get", "/email_sender/get",
dependencies=[Depends(tenant_only)], dependencies=[Depends(tenant_only)],
operation_id="get_email_senders", operation_id="get_email_sender",
summary="Get email senders for tenant", summary="Get email sender for tenant",
description="Retrieve the list of email senders configured for the current tenant", description="Retrieve the email sender configured for the current tenant",
response_description="List of email sender addresses" response_description="email sender address"
) )
async def get_email_senders(payload: dict = Depends(tenant_only)): async def get_email_sender(payload: dict = Depends(tenant_only)):
try: try:
tenant_id = payload.get("tenant_id") tenant_id = payload.get("tenant_id")
result = await email_sender_hub.get_email_senders(tenant_id) email_sender = await email_sender_hub.get_email_sender(tenant_id)
return JSONResponse( return JSONResponse(
content={"success": True, "email_senders": result}, content={"success": True, "email_sender": email_sender},
status_code=200 status_code=200
) )
except ValueError as e: except ValueError as e:
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail="Failed to get email senders") raise HTTPException(status_code=500, detail="Failed to get email sender")
# Set email senders for tenant # Set email sender for tenant
@router.post( @router.post(
"/email_senders/set", "/email_sender/set",
dependencies=[Depends(tenant_only)], dependencies=[Depends(tenant_only)],
operation_id="set_email_senders", operation_id="set_email_sender",
summary="Set email senders for tenant", summary="Set email sender for tenant",
description="Set the complete list of email senders for the specified tenant", description="Set the email sender for the specified tenant (replaces existing)",
response_description="Success/failure response in setting email senders" response_description="Success/failure response in setting email sender"
) )
async def set_email_senders(request: EmailSenderSetRequest, payload: dict = Depends(tenant_only)): async def set_email_sender(request: EmailSenderRequest, payload: dict = Depends(tenant_only)):
try: try:
tenant_id = payload.get("tenant_id") tenant_id = payload.get("tenant_id")
result = await email_sender_hub.set_email_senders(tenant_id, request.email_senders) email_sender = await email_sender_hub.set_email_sender(tenant_id, request.email_sender)
return JSONResponse( return JSONResponse(
content=result, content=email_sender,
status_code=200 status_code=200
) )
except ValueError as e: except ValueError as e:
@ -119,24 +114,23 @@ async def set_email_senders(request: EmailSenderSetRequest, payload: dict = Depe
except Exception as e: except Exception as e:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Failed to set email senders: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to set email sender: {str(e)}")
# Add email senders to tenant # Update email sender for tenant
@router.post( @router.put(
"/email_senders/add", "/email_sender/update",
dependencies=[Depends(tenant_only)], dependencies=[Depends(tenant_only)],
operation_id="add_email_senders", operation_id="update_email_sender",
summary="Add email senders to tenant", summary="Update email sender for tenant",
description="Add new email senders to the existing list for the specified tenant", description="Update the email sender for the specified tenant (only if exists)",
response_description="Success/failure response in adding email senders" response_description="Success/failure response in updating email sender"
) )
async def add_email_senders(request: EmailSenderAddRequest, payload: dict = Depends(tenant_only)): async def update_email_sender(request: EmailSenderRequest, payload: dict = Depends(tenant_only)):
try: try:
tenant_id = payload.get("tenant_id") tenant_id = payload.get("tenant_id")
email_sender = await email_sender_hub.update_email_sender(tenant_id, request.email_sender)
result = await email_sender_hub.add_email_senders(tenant_id, request.new_senders)
return JSONResponse( return JSONResponse(
content=result, content=email_sender,
status_code=200 status_code=200
) )
except ValueError as e: except ValueError as e:
@ -144,57 +138,11 @@ async def add_email_senders(request: EmailSenderAddRequest, payload: dict = Depe
except Exception as e: except Exception as e:
import traceback import traceback
traceback.print_exc() traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Failed to add email senders: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to update email sender: {str(e)}")
# Remove email senders from tenant
@router.delete(
"/email_senders/remove",
dependencies=[Depends(tenant_only)],
operation_id="remove_email_senders",
summary="Remove email senders from tenant",
description="Remove specific email senders from the tenant's list",
response_description="Success/failure response in removing email senders"
)
async def remove_email_senders(request: EmailSenderRemoveRequest, payload: dict = Depends(tenant_only)):
try:
tenant_id = payload.get("tenant_id")
result = await email_sender_hub.remove_email_senders(tenant_id, request.emails_to_remove)
return JSONResponse(
content=result,
status_code=200
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Failed to remove email senders: {str(e)}")
# Clear all email senders for tenant
@router.delete(
"/email_senders/clear",
dependencies=[Depends(tenant_only)],
operation_id="clear_email_senders",
summary="Clear all email senders for tenant",
description="Remove all email senders from the current tenant's list",
response_description="Success/failure response in clearing email senders"
)
async def clear_email_senders(payload: dict = Depends(tenant_only)):
try:
tenant_id = payload.get("tenant_id")
result = await email_sender_hub.clear_email_senders(tenant_id)
return JSONResponse(
content=result,
status_code=200
)
except Exception as e:
raise HTTPException(status_code=500, detail="Failed to clear email senders")
# Delete email sender configuration for tenant # Delete email sender configuration for tenant
@router.delete( @router.delete(
"/email_senders/delete/{tenant_id}", "/email_sender/delete/{tenant_id}",
dependencies=[Depends(admin_only)], dependencies=[Depends(admin_only)],
operation_id="delete_email_sender", operation_id="delete_email_sender",
summary="Delete email sender configuration for tenant", summary="Delete email sender configuration for tenant",

View File

@ -16,15 +16,15 @@ class TenantEmailRequest(BaseModel):
subject_properties: Dict = {} subject_properties: Dict = {}
body_properties: Dict = {} body_properties: Dict = {}
region: int region: int
sender_emails: Optional[List[str]] = None sender_email: str = None
priority: str = "normal" priority: str = "normal"
tracking_enabled: bool = True tracking_enabled: bool = True
@router.post( @router.post(
"/send_tenant_email", "/send_tenant_email",
operation_id="send_tenant_email", operation_id="send_tenant_email",
summary="Send email using tenant's template and email senders", summary="Send email using tenant's template and email sender",
description="Send email using tenant's selected template and email senders", description="Send email using tenant's selected template and email sender",
response_description="Success/failure response in processing the tenant email send request", response_description="Success/failure response in processing the tenant email send request",
) )
async def send_tenant_email(request: TenantEmailRequest): async def send_tenant_email(request: TenantEmailRequest):
@ -36,7 +36,7 @@ async def send_tenant_email(request: TenantEmailRequest):
subject_properties=request.subject_properties, subject_properties=request.subject_properties,
body_properties=request.body_properties, body_properties=request.body_properties,
region=request.region, region=request.region,
sender_emails=request.sender_emails, sender_email=request.sender_email,
priority=request.priority, priority=request.priority,
tracking_enabled=request.tracking_enabled tracking_enabled=request.tracking_enabled
) )
@ -87,4 +87,4 @@ async def get_tenant_email_status_list(
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get tenant email status list: {str(e)}") raise HTTPException(status_code=500, detail=f"Failed to get tenant email status list: {str(e)}")
## TODO: add SendGrid Event Webhook to handle bounce and tracking

View File

@ -60,7 +60,7 @@ class EmailMQConsumer:
tenant_id = message.get("tenant_id") or message.get("properties", {}).get("tenant_id") tenant_id = message.get("tenant_id") or message.get("properties", {}).get("tenant_id")
template_id = message.get("properties", {}).get("template_id") template_id = message.get("properties", {}).get("template_id")
destination_emails = message.get("properties", {}).get("destination_emails", []) destination_emails = message.get("properties", {}).get("destination_emails", [])
sender_emails = message.get("properties", {}).get("sender_emails", []) sender_email = message.get("properties", {}).get("sender_email", "")
# Use rendered content instead of template properties # Use rendered content instead of template properties
subject = message.get("properties", {}).get("content_subject", "No Subject") subject = message.get("properties", {}).get("content_subject", "No Subject")
html_content = message.get("properties", {}).get("content_text", "") html_content = message.get("properties", {}).get("content_text", "")
@ -74,7 +74,7 @@ class EmailMQConsumer:
tenant_id=tenant_id, tenant_id=tenant_id,
template_id=template_id, template_id=template_id,
recipient_email=recipient_email, recipient_email=recipient_email,
sender_emails=sender_emails, sender_email=sender_email,
subject_properties={"subject": subject}, subject_properties={"subject": subject},
body_properties={"html_content": html_content} body_properties={"html_content": html_content}
) )