From 370cd61fd20f6dc7b33122d7bf0604a5776f5f50 Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Thu, 14 Aug 2025 21:08:43 +0800 Subject: [PATCH] refactor(email_sender): change email_sender type from list to str --- .../backend/application/email_sender_hub.py | 41 ++-- .../application/tenant_notification_hub.py | 29 ++- .../backend/business/email_sender_manager.py | 86 ++------- .../business/tenant_notification_manager.py | 29 ++- .../backend/infra/email_handler.py | 41 +++- .../backend/infra/email_sender_handler.py | 127 ++++--------- apps/notification/backend/models/models.py | 4 +- .../email/email_validation_service.py | 41 ++-- .../backend/services/email_sender_service.py | 56 +++--- .../webapi/routes/email_sender.py | 178 +++++++----------- .../webapi/routes/tenant_notification.py | 10 +- .../webapi/utils/email_consumer.py | 4 +- 12 files changed, 248 insertions(+), 398 deletions(-) diff --git a/apps/notification/backend/application/email_sender_hub.py b/apps/notification/backend/application/email_sender_hub.py index 1c3d623..f9fdde2 100644 --- a/apps/notification/backend/application/email_sender_hub.py +++ b/apps/notification/backend/application/email_sender_hub.py @@ -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""" diff --git a/apps/notification/backend/application/tenant_notification_hub.py b/apps/notification/backend/application/tenant_notification_hub.py index 00a8bef..a1d2896 100644 --- a/apps/notification/backend/application/tenant_notification_hub.py +++ b/apps/notification/backend/application/tenant_notification_hub.py @@ -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") } ) diff --git a/apps/notification/backend/business/email_sender_manager.py b/apps/notification/backend/business/email_sender_manager.py index da8b5df..8f68199 100644 --- a/apps/notification/backend/business/email_sender_manager.py +++ b/apps/notification/backend/business/email_sender_manager.py @@ -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 + diff --git a/apps/notification/backend/business/tenant_notification_manager.py b/apps/notification/backend/business/tenant_notification_manager.py index e458045..0353faf 100644 --- a/apps/notification/backend/business/tenant_notification_manager.py +++ b/apps/notification/backend/business/tenant_notification_manager.py @@ -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 } } diff --git a/apps/notification/backend/infra/email_handler.py b/apps/notification/backend/infra/email_handler.py index 2c70ce5..aee7966 100644 --- a/apps/notification/backend/infra/email_handler.py +++ b/apps/notification/backend/infra/email_handler.py @@ -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={ diff --git a/apps/notification/backend/infra/email_sender_handler.py b/apps/notification/backend/infra/email_sender_handler.py index 8283613..e38e344 100644 --- a/apps/notification/backend/infra/email_sender_handler.py +++ b/apps/notification/backend/infra/email_sender_handler.py @@ -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,76 +54,25 @@ class EmailSenderHandler: ) raise - async def add_email_senders(self, tenant_id: str, new_senders: List[str]): - """add email senders to tenant""" - 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}) - await self.module_logger.log_info( - info="Email senders added to database", - properties={ - "tenant_id": tenant_id, - "added_count": len(to_add), - "total_count": len(updated_list) - } - ) - 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} - except Exception as e: - await self.module_logger.log_error( - error="Failed to add email senders", - properties={ - "tenant_id": tenant_id, - "error": str(e) - } - ) - raise - - async def remove_email_senders(self, tenant_id: str, emails_to_remove: List[str]): - """remove email senders from tenant""" + async def update_email_sender(self, tenant_id: str, email_sender: str): + """update email sender for tenant (only if exists)""" try: doc = await EmailSenderDoc.find_one({"tenant_id": tenant_id, "is_active": True}) - if not doc or not doc.email_senders: - return {"success": False, "msg": "No sender found"} + if not doc: + raise ValueError("Email sender configuration not 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"} - - await doc.set({"email_senders": doc.email_senders}) + await doc.set({"email_sender": email_sender}) await self.module_logger.log_info( - info="Email senders removed from database", + info="Email sender updated in database", properties={ "tenant_id": tenant_id, - "removed_count": original_count - len(doc.email_senders), - "remaining_count": len(doc.email_senders) + "email_sender": email_sender } ) - return {"success": True, "remaining": doc.email_senders} + return {"success": True, "email_sender": email_sender} except Exception as e: await self.module_logger.log_error( - error="Failed to remove email senders", + error="Failed to update email sender", properties={ "tenant_id": tenant_id, "error": str(e) @@ -131,21 +80,27 @@ class EmailSenderHandler: ) raise - async def clear_email_senders(self, tenant_id: str): - """clear up email senders for 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 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"} + if not doc or not doc.email_sender: + return {"success": False, "msg": "No sender found"} + + original_email_sender = doc.email_sender + + await doc.set({"email_sender": None}) + await self.module_logger.log_info( + info="Email sender removed from database", + properties={ + "tenant_id": tenant_id, + "removed_email_sender": original_email_sender + } + ) + return {"success": True, "removed_email_sender": original_email_sender} 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) diff --git a/apps/notification/backend/models/models.py b/apps/notification/backend/models/models.py index e619e71..49c3cf2 100644 --- a/apps/notification/backend/models/models.py +++ b/apps/notification/backend/models/models.py @@ -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 diff --git a/apps/notification/backend/services/email/email_validation_service.py b/apps/notification/backend/services/email/email_validation_service.py index 4e6d17b..11b9881 100644 --- a/apps/notification/backend/services/email/email_validation_service.py +++ b/apps/notification/backend/services/email/email_validation_service.py @@ -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: - # format validation - if not await self.email_validation_handler.is_valid_email(sender_email): - invalid_senders.append(sender_email) - continue + 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): + return None - # domain validation - if not await self.email_validation_handler.is_valid_domain(sender_email): - invalid_senders.append(sender_email) - continue - - # 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) + # sender permission validation + # Allow support@freeleaps.com as default sender even if not in authorized_senders + 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( diff --git a/apps/notification/backend/services/email_sender_service.py b/apps/notification/backend/services/email_sender_service.py index 121ee0c..af81712 100644 --- a/apps/notification/backend/services/email_sender_service.py +++ b/apps/notification/backend/services/email_sender_service.py @@ -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") + + # 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 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)""" 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"} - - return await self.email_sender_handler.add_email_senders(tenant_id, new_senders) + if not email_sender: + raise ValueError("email_sender must be provided") - 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_handler.remove_email_senders(tenant_id, emails_to_remove) + if not await self.email_validation_handler.is_valid_email(email_sender): + raise ValueError("Invalid email format") - async def clear_email_senders(self, tenant_id: str): - """clear email senders for 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") - 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""" diff --git a/apps/notification/webapi/routes/email_sender.py b/apps/notification/webapi/routes/email_sender.py index dbf3e04..2ad52f8 100644 --- a/apps/notification/webapi/routes/email_sender.py +++ b/apps/notification/webapi/routes/email_sender.py @@ -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,127 +68,81 @@ 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) - return JSONResponse( - content={"success": True, "email_senders": result}, - 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") - -# Set email senders for tenant -@router.post( - "/email_senders/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" -) -async def set_email_senders(request: EmailSenderSetRequest, 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) - 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 set email senders: {str(e)}") - -# Add email senders to tenant -@router.post( - "/email_senders/add", - 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" -) -async def add_email_senders(request: EmailSenderAddRequest, 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) - 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 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") + email_sender = await email_sender_hub.get_email_sender(tenant_id) - result = await email_sender_hub.clear_email_senders(tenant_id) return JSONResponse( - content=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 clear email senders") + raise HTTPException(status_code=500, detail="Failed to get email sender") + +# Set email sender for tenant +@router.post( + "/email_sender/set", + dependencies=[Depends(tenant_only)], + 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_sender(request: EmailSenderRequest, payload: dict = Depends(tenant_only)): + try: + tenant_id = payload.get("tenant_id") + email_sender = await email_sender_hub.set_email_sender(tenant_id, request.email_sender) + return JSONResponse( + content=email_sender, + 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 set email sender: {str(e)}") + +# Update email sender for tenant +@router.put( + "/email_sender/update", + dependencies=[Depends(tenant_only)], + 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 update_email_sender(request: EmailSenderRequest, payload: dict = Depends(tenant_only)): + try: + tenant_id = payload.get("tenant_id") + email_sender = await email_sender_hub.update_email_sender(tenant_id, request.email_sender) + return JSONResponse( + content=email_sender, + 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 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", diff --git a/apps/notification/webapi/routes/tenant_notification.py b/apps/notification/webapi/routes/tenant_notification.py index c7987ff..bfb18ef 100644 --- a/apps/notification/webapi/routes/tenant_notification.py +++ b/apps/notification/webapi/routes/tenant_notification.py @@ -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)}") - \ No newline at end of file + ## TODO: add SendGrid Event Webhook to handle bounce and tracking \ No newline at end of file diff --git a/apps/notification/webapi/utils/email_consumer.py b/apps/notification/webapi/utils/email_consumer.py index 3a8a96e..f2abf88 100644 --- a/apps/notification/webapi/utils/email_consumer.py +++ b/apps/notification/webapi/utils/email_consumer.py @@ -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} )