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):
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"""
if not tenant_id:
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]):
"""set email senders for tenant"""
async def set_email_sender(self, tenant_id: str, email_sender: str):
"""set email sender for tenant"""
if not tenant_id:
raise ValueError("tenant_id is required")
if not email_senders or not isinstance(email_senders, list):
raise ValueError("email_senders must be a non-empty list")
if not email_sender:
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]):
"""add email senders to tenant"""
async def update_email_sender(self, tenant_id: str, email_sender: str):
"""update email sender for tenant"""
if not tenant_id:
raise ValueError("tenant_id is required")
if not new_senders or not isinstance(new_senders, list):
raise ValueError("new_senders must be a non-empty list")
if not email_sender:
raise ValueError("email_sender must be provided")
return await self.email_sender_manager.add_email_senders(tenant_id, new_senders)
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)
return await self.email_sender_manager.update_email_sender(tenant_id, email_sender)
async def delete_email_sender(self, tenant_id: str):
"""delete email sender for tenant"""

View File

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

View File

@ -10,90 +10,49 @@ class EmailSenderManager:
self.email_sender_service = EmailSenderService()
self.module_logger = ModuleLogger(sender_id="EmailSenderManager")
async def get_email_senders(self, tenant_id: str):
"""get email senders for tenant"""
email_senders = await self.email_sender_service.get_email_senders(tenant_id)
async def get_email_sender(self, tenant_id: str):
"""get email sendersfor tenant"""
email_sender = await self.email_sender_service.get_email_sender(tenant_id)
await self.module_logger.log_info(
info="Email senders retrieved",
info="Email sender retrieved",
properties={
"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]):
"""set email senders for tenant"""
if not email_senders:
raise ValueError("Email senders list cannot be empty")
async def set_email_sender(self, tenant_id: str, email_sender: str):
"""set email sender for tenant"""
if not email_sender:
raise ValueError("Email sender must be provided")
for email in email_senders:
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)
result = await self.email_sender_service.set_email_sender(tenant_id, email_sender)
await self.module_logger.log_info(
info="Email senders set",
properties={
"tenant_id": tenant_id,
"sender_count": len(email_senders)
"email_sender": email_sender
}
)
return result
async def add_email_senders(self, tenant_id: str, new_senders: List[str]):
"""add email senders to tenant"""
if not new_senders:
raise ValueError("New senders list cannot be empty")
async def update_email_sender(self, tenant_id: str, email_sender: str):
"""update email sender for tenant"""
if not email_sender:
raise ValueError("Email sender must be provided")
for email in new_senders:
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)
result = await self.email_sender_service.update_email_sender(tenant_id, email_sender)
await self.module_logger.log_info(
info="Email senders added",
info="Email senders set",
properties={
"tenant_id": tenant_id,
"new_sender_count": len(new_senders),
"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)
"email_sender": email_sender
}
)
@ -113,12 +72,7 @@ class EmailSenderManager:
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,
rendered_template: Dict,
recipient_emails: List[str],
sender_emails: List[str],
sender_email: str,
region: int,
priority: str = "normal",
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:
# 1. validate 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:
raise InvalidDataError("No valid recipient emails found")
if not valid_senders:
raise InvalidDataError("No valid sender emails found")
if valid_sender is None:
raise InvalidDataError("Invalid sender email")
# 2. check blacklisted recipients
blacklisted_recipients = []
@ -58,7 +58,7 @@ class TenantNotificationManager:
raise InvalidDataError("All recipient emails are blacklisted")
# 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"]:
raise InvalidDataError("Rate limit exceeded")
@ -76,7 +76,7 @@ class TenantNotificationManager:
"tenant_id": tenant_id,
"template_id": template_id,
"destination_emails": valid_recipients,
"sender_emails": valid_senders,
"sender_email": valid_sender,
"subject_properties": rendered_template.get("subject_properties", {}),
"body_properties": rendered_template.get("body_properties", {}),
"region": region,
@ -87,7 +87,7 @@ class TenantNotificationManager:
"receiver_type": "email",
"validation_info": {
"invalid_recipients": invalid_recipients,
"invalid_senders": invalid_senders,
"invalid_sender": not valid_sender,
"blacklisted_recipients": blacklisted_recipients,
"rate_limit_info": rate_limit_result,
"spam_detection_info": spam_result
@ -101,7 +101,7 @@ class TenantNotificationManager:
"tenant_id": tenant_id,
"template_id": template_id,
"destination_emails": [recipient_email],
"sender_emails": valid_senders,
"sender_email": valid_sender,
"subject_properties": rendered_template.get("subject_properties", {}),
"body_properties": rendered_template.get("body_properties", {}),
"region": region,
@ -112,7 +112,7 @@ class TenantNotificationManager:
"receiver_type": "email",
"validation_info": {
"invalid_recipients": invalid_recipients,
"invalid_senders": invalid_senders,
"invalid_sender": not valid_sender,
"blacklisted_recipients": blacklisted_recipients,
"rate_limit_info": rate_limit_result,
"spam_detection_info": spam_result
@ -138,8 +138,8 @@ class TenantNotificationManager:
"valid_recipient_count": len(valid_recipients),
"invalid_recipient_count": len(invalid_recipients),
"blacklisted_recipient_count": len(blacklisted_recipients),
"valid_sender_count": len(valid_senders),
"invalid_sender_count": len(invalid_senders)
"sender_email": sender_email,
"sender_valid": valid_sender
}
)
@ -154,9 +154,8 @@ class TenantNotificationManager:
"valid_recipients": len(valid_recipients),
"invalid_recipients": len(invalid_recipients),
"blacklisted_recipients": len(blacklisted_recipients),
"total_senders": len(sender_emails),
"valid_senders": len(valid_senders),
"invalid_senders": len(invalid_senders)
"sender_email": sender_email,
"sender_valid": valid_sender
}
}

View File

@ -49,17 +49,18 @@ class EmailHandler:
tenant_id: str,
template_id: str,
recipient_email: str,
sender_emails: List[str],
sender_email: str,
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")
try:
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")
html_content = body_properties.get("html_content", "")
@ -68,7 +69,7 @@ class EmailHandler:
email_status_doc = EmailSendStatusDoc(
email_id=email_id,
tenant_id=tenant_id,
email_senders=sender_emails,
email_sender=sender_email,
recipient_email=recipient_email,
template_id=template_id,
subject=subject,
@ -77,6 +78,20 @@ class EmailHandler:
)
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(
from_email=from_email,
to_emails=recipient_email,
@ -84,6 +99,17 @@ class EmailHandler:
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)
response = sg.send(mail)
@ -92,6 +118,11 @@ class EmailHandler:
email_status_doc.message_id = str(response.headers.get('X-Message-Id', ''))
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(
f"Tenant email sent successfully",
properties={

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,15 +16,15 @@ class TenantEmailRequest(BaseModel):
subject_properties: Dict = {}
body_properties: Dict = {}
region: int
sender_emails: Optional[List[str]] = None
sender_email: str = None
priority: str = "normal"
tracking_enabled: bool = True
@router.post(
"/send_tenant_email",
operation_id="send_tenant_email",
summary="Send email using tenant's template and email senders",
description="Send email using tenant's selected template and email senders",
summary="Send email using tenant's template and email sender",
description="Send email using tenant's selected template and email sender",
response_description="Success/failure response in processing the tenant email send request",
)
async def send_tenant_email(request: TenantEmailRequest):
@ -36,7 +36,7 @@ async def send_tenant_email(request: TenantEmailRequest):
subject_properties=request.subject_properties,
body_properties=request.body_properties,
region=request.region,
sender_emails=request.sender_emails,
sender_email=request.sender_email,
priority=request.priority,
tracking_enabled=request.tracking_enabled
)
@ -87,4 +87,4 @@ async def get_tenant_email_status_list(
except Exception as 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")
template_id = message.get("properties", {}).get("template_id")
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
subject = message.get("properties", {}).get("content_subject", "No Subject")
html_content = message.get("properties", {}).get("content_text", "")
@ -74,7 +74,7 @@ class EmailMQConsumer:
tenant_id=tenant_id,
template_id=template_id,
recipient_email=recipient_email,
sender_emails=sender_emails,
sender_email=sender_email,
subject_properties={"subject": subject},
body_properties={"html_content": html_content}
)