refactor: centralize email internationalization handling (#22752)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
461
api/libs/email_i18n.py
Normal file
461
api/libs/email_i18n.py
Normal file
@@ -0,0 +1,461 @@
|
||||
"""
|
||||
Email Internationalization Module
|
||||
|
||||
This module provides a centralized, elegant way to handle email internationalization
|
||||
in Dify. It follows Domain-Driven Design principles with proper type hints and
|
||||
eliminates the need for repetitive language switching logic.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
from flask import render_template
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from services.feature_service import BrandingModel, FeatureService
|
||||
|
||||
|
||||
class EmailType(Enum):
|
||||
"""Enumeration of supported email types."""
|
||||
|
||||
RESET_PASSWORD = "reset_password"
|
||||
INVITE_MEMBER = "invite_member"
|
||||
EMAIL_CODE_LOGIN = "email_code_login"
|
||||
CHANGE_EMAIL_OLD = "change_email_old"
|
||||
CHANGE_EMAIL_NEW = "change_email_new"
|
||||
OWNER_TRANSFER_CONFIRM = "owner_transfer_confirm"
|
||||
OWNER_TRANSFER_OLD_NOTIFY = "owner_transfer_old_notify"
|
||||
OWNER_TRANSFER_NEW_NOTIFY = "owner_transfer_new_notify"
|
||||
ACCOUNT_DELETION_SUCCESS = "account_deletion_success"
|
||||
ACCOUNT_DELETION_VERIFICATION = "account_deletion_verification"
|
||||
ENTERPRISE_CUSTOM = "enterprise_custom"
|
||||
QUEUE_MONITOR_ALERT = "queue_monitor_alert"
|
||||
DOCUMENT_CLEAN_NOTIFY = "document_clean_notify"
|
||||
|
||||
|
||||
class EmailLanguage(Enum):
|
||||
"""Supported email languages with fallback handling."""
|
||||
|
||||
EN_US = "en-US"
|
||||
ZH_HANS = "zh-Hans"
|
||||
|
||||
@classmethod
|
||||
def from_language_code(cls, language_code: str) -> "EmailLanguage":
|
||||
"""Convert a language code to EmailLanguage with fallback to English."""
|
||||
if language_code == "zh-Hans":
|
||||
return cls.ZH_HANS
|
||||
return cls.EN_US
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmailTemplate:
|
||||
"""Immutable value object representing an email template configuration."""
|
||||
|
||||
subject: str
|
||||
template_path: str
|
||||
branded_template_path: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmailContent:
|
||||
"""Immutable value object containing rendered email content."""
|
||||
|
||||
subject: str
|
||||
html_content: str
|
||||
template_context: dict[str, Any]
|
||||
|
||||
|
||||
class EmailI18nConfig(BaseModel):
|
||||
"""Configuration for email internationalization."""
|
||||
|
||||
model_config = {"frozen": True, "extra": "forbid"}
|
||||
|
||||
templates: dict[EmailType, dict[EmailLanguage, EmailTemplate]] = Field(
|
||||
default_factory=dict, description="Mapping of email types to language-specific templates"
|
||||
)
|
||||
|
||||
def get_template(self, email_type: EmailType, language: EmailLanguage) -> EmailTemplate:
|
||||
"""Get template configuration for specific email type and language."""
|
||||
type_templates = self.templates.get(email_type)
|
||||
if not type_templates:
|
||||
raise ValueError(f"No templates configured for email type: {email_type}")
|
||||
|
||||
template = type_templates.get(language)
|
||||
if not template:
|
||||
# Fallback to English if specific language not found
|
||||
template = type_templates.get(EmailLanguage.EN_US)
|
||||
if not template:
|
||||
raise ValueError(f"No template found for {email_type} in {language} or English")
|
||||
|
||||
return template
|
||||
|
||||
|
||||
class EmailRenderer(Protocol):
|
||||
"""Protocol for email template renderers."""
|
||||
|
||||
def render_template(self, template_path: str, **context: Any) -> str:
|
||||
"""Render email template with given context."""
|
||||
...
|
||||
|
||||
|
||||
class FlaskEmailRenderer:
|
||||
"""Flask-based email template renderer."""
|
||||
|
||||
def render_template(self, template_path: str, **context: Any) -> str:
|
||||
"""Render email template using Flask's render_template."""
|
||||
return render_template(template_path, **context)
|
||||
|
||||
|
||||
class BrandingService(Protocol):
|
||||
"""Protocol for branding service abstraction."""
|
||||
|
||||
def get_branding_config(self) -> BrandingModel:
|
||||
"""Get current branding configuration."""
|
||||
...
|
||||
|
||||
|
||||
class FeatureBrandingService:
|
||||
"""Feature service based branding implementation."""
|
||||
|
||||
def get_branding_config(self) -> BrandingModel:
|
||||
"""Get branding configuration from feature service."""
|
||||
return FeatureService.get_system_features().branding
|
||||
|
||||
|
||||
class EmailSender(Protocol):
|
||||
"""Protocol for email sending abstraction."""
|
||||
|
||||
def send_email(self, to: str, subject: str, html_content: str) -> None:
|
||||
"""Send email with given parameters."""
|
||||
...
|
||||
|
||||
|
||||
class FlaskMailSender:
|
||||
"""Flask-Mail based email sender."""
|
||||
|
||||
def send_email(self, to: str, subject: str, html_content: str) -> None:
|
||||
"""Send email using Flask-Mail."""
|
||||
if mail.is_inited():
|
||||
mail.send(to=to, subject=subject, html=html_content)
|
||||
|
||||
|
||||
class EmailI18nService:
|
||||
"""
|
||||
Main service for internationalized email handling.
|
||||
|
||||
This service provides a clean API for sending internationalized emails
|
||||
with proper branding support and template management.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: EmailI18nConfig,
|
||||
renderer: EmailRenderer,
|
||||
branding_service: BrandingService,
|
||||
sender: EmailSender,
|
||||
) -> None:
|
||||
self._config = config
|
||||
self._renderer = renderer
|
||||
self._branding_service = branding_service
|
||||
self._sender = sender
|
||||
|
||||
def send_email(
|
||||
self,
|
||||
email_type: EmailType,
|
||||
language_code: str,
|
||||
to: str,
|
||||
template_context: Optional[dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Send internationalized email with branding support.
|
||||
|
||||
Args:
|
||||
email_type: Type of email to send
|
||||
language_code: Target language code
|
||||
to: Recipient email address
|
||||
template_context: Additional context for template rendering
|
||||
"""
|
||||
if template_context is None:
|
||||
template_context = {}
|
||||
|
||||
language = EmailLanguage.from_language_code(language_code)
|
||||
email_content = self._render_email_content(email_type, language, template_context)
|
||||
|
||||
self._sender.send_email(to=to, subject=email_content.subject, html_content=email_content.html_content)
|
||||
|
||||
def send_change_email(
|
||||
self,
|
||||
language_code: str,
|
||||
to: str,
|
||||
code: str,
|
||||
phase: str,
|
||||
) -> None:
|
||||
"""
|
||||
Send change email notification with phase-specific handling.
|
||||
|
||||
Args:
|
||||
language_code: Target language code
|
||||
to: Recipient email address
|
||||
code: Verification code
|
||||
phase: Either 'old_email' or 'new_email'
|
||||
"""
|
||||
if phase == "old_email":
|
||||
email_type = EmailType.CHANGE_EMAIL_OLD
|
||||
elif phase == "new_email":
|
||||
email_type = EmailType.CHANGE_EMAIL_NEW
|
||||
else:
|
||||
raise ValueError(f"Invalid phase: {phase}. Must be 'old_email' or 'new_email'")
|
||||
|
||||
self.send_email(
|
||||
email_type=email_type,
|
||||
language_code=language_code,
|
||||
to=to,
|
||||
template_context={
|
||||
"to": to,
|
||||
"code": code,
|
||||
},
|
||||
)
|
||||
|
||||
def send_raw_email(
|
||||
self,
|
||||
to: str | list[str],
|
||||
subject: str,
|
||||
html_content: str,
|
||||
) -> None:
|
||||
"""
|
||||
Send a raw email directly without template processing.
|
||||
|
||||
This method is provided for backward compatibility with legacy email
|
||||
sending that uses pre-rendered HTML content (e.g., enterprise emails
|
||||
with custom templates).
|
||||
|
||||
Args:
|
||||
to: Recipient email address(es)
|
||||
subject: Email subject
|
||||
html_content: Pre-rendered HTML content
|
||||
"""
|
||||
if isinstance(to, list):
|
||||
for recipient in to:
|
||||
self._sender.send_email(to=recipient, subject=subject, html_content=html_content)
|
||||
else:
|
||||
self._sender.send_email(to=to, subject=subject, html_content=html_content)
|
||||
|
||||
def _render_email_content(
|
||||
self,
|
||||
email_type: EmailType,
|
||||
language: EmailLanguage,
|
||||
template_context: dict[str, Any],
|
||||
) -> EmailContent:
|
||||
"""Render email content with branding and internationalization."""
|
||||
template_config = self._config.get_template(email_type, language)
|
||||
branding = self._branding_service.get_branding_config()
|
||||
|
||||
# Determine template path based on branding
|
||||
template_path = template_config.branded_template_path if branding.enabled else template_config.template_path
|
||||
|
||||
# Prepare template context with branding information
|
||||
full_context = {
|
||||
**template_context,
|
||||
"branding_enabled": branding.enabled,
|
||||
"application_title": branding.application_title if branding.enabled else "Dify",
|
||||
}
|
||||
|
||||
# Render template
|
||||
html_content = self._renderer.render_template(template_path, **full_context)
|
||||
|
||||
# Apply templating to subject with all context variables
|
||||
subject = template_config.subject
|
||||
try:
|
||||
subject = subject.format(**full_context)
|
||||
except KeyError:
|
||||
# If template variables are missing, fall back to basic formatting
|
||||
if branding.enabled and "{application_title}" in subject:
|
||||
subject = subject.format(application_title=branding.application_title)
|
||||
|
||||
return EmailContent(
|
||||
subject=subject,
|
||||
html_content=html_content,
|
||||
template_context=full_context,
|
||||
)
|
||||
|
||||
|
||||
def create_default_email_config() -> EmailI18nConfig:
|
||||
"""Create default email i18n configuration with all supported templates."""
|
||||
templates: dict[EmailType, dict[EmailLanguage, EmailTemplate]] = {
|
||||
EmailType.RESET_PASSWORD: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Set Your {application_title} Password",
|
||||
template_path="reset_password_mail_template_en-US.html",
|
||||
branded_template_path="without-brand/reset_password_mail_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="设置您的 {application_title} 密码",
|
||||
template_path="reset_password_mail_template_zh-CN.html",
|
||||
branded_template_path="without-brand/reset_password_mail_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.INVITE_MEMBER: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Join {application_title} Workspace Now",
|
||||
template_path="invite_member_mail_template_en-US.html",
|
||||
branded_template_path="without-brand/invite_member_mail_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="立即加入 {application_title} 工作空间",
|
||||
template_path="invite_member_mail_template_zh-CN.html",
|
||||
branded_template_path="without-brand/invite_member_mail_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.EMAIL_CODE_LOGIN: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="{application_title} Login Code",
|
||||
template_path="email_code_login_mail_template_en-US.html",
|
||||
branded_template_path="without-brand/email_code_login_mail_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="{application_title} 登录验证码",
|
||||
template_path="email_code_login_mail_template_zh-CN.html",
|
||||
branded_template_path="without-brand/email_code_login_mail_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.CHANGE_EMAIL_OLD: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Check your current email",
|
||||
template_path="change_mail_confirm_old_template_en-US.html",
|
||||
branded_template_path="without-brand/change_mail_confirm_old_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="检测您现在的邮箱",
|
||||
template_path="change_mail_confirm_old_template_zh-CN.html",
|
||||
branded_template_path="without-brand/change_mail_confirm_old_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.CHANGE_EMAIL_NEW: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Confirm your new email address",
|
||||
template_path="change_mail_confirm_new_template_en-US.html",
|
||||
branded_template_path="without-brand/change_mail_confirm_new_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="确认您的邮箱地址变更",
|
||||
template_path="change_mail_confirm_new_template_zh-CN.html",
|
||||
branded_template_path="without-brand/change_mail_confirm_new_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.OWNER_TRANSFER_CONFIRM: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Verify Your Request to Transfer Workspace Ownership",
|
||||
template_path="transfer_workspace_owner_confirm_template_en-US.html",
|
||||
branded_template_path="without-brand/transfer_workspace_owner_confirm_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="验证您转移工作空间所有权的请求",
|
||||
template_path="transfer_workspace_owner_confirm_template_zh-CN.html",
|
||||
branded_template_path="without-brand/transfer_workspace_owner_confirm_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.OWNER_TRANSFER_OLD_NOTIFY: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Workspace ownership has been transferred",
|
||||
template_path="transfer_workspace_old_owner_notify_template_en-US.html",
|
||||
branded_template_path="without-brand/transfer_workspace_old_owner_notify_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="工作区所有权已转移",
|
||||
template_path="transfer_workspace_old_owner_notify_template_zh-CN.html",
|
||||
branded_template_path="without-brand/transfer_workspace_old_owner_notify_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.OWNER_TRANSFER_NEW_NOTIFY: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="You are now the owner of {WorkspaceName}",
|
||||
template_path="transfer_workspace_new_owner_notify_template_en-US.html",
|
||||
branded_template_path="without-brand/transfer_workspace_new_owner_notify_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="您现在是 {WorkspaceName} 的所有者",
|
||||
template_path="transfer_workspace_new_owner_notify_template_zh-CN.html",
|
||||
branded_template_path="without-brand/transfer_workspace_new_owner_notify_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.ACCOUNT_DELETION_SUCCESS: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Your Dify.AI Account Has Been Successfully Deleted",
|
||||
template_path="delete_account_success_template_en-US.html",
|
||||
branded_template_path="delete_account_success_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="您的 Dify.AI 账户已成功删除",
|
||||
template_path="delete_account_success_template_zh-CN.html",
|
||||
branded_template_path="delete_account_success_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.ACCOUNT_DELETION_VERIFICATION: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Dify.AI Account Deletion and Verification",
|
||||
template_path="delete_account_code_email_template_en-US.html",
|
||||
branded_template_path="delete_account_code_email_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="Dify.AI 账户删除和验证",
|
||||
template_path="delete_account_code_email_template_zh-CN.html",
|
||||
branded_template_path="delete_account_code_email_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.QUEUE_MONITOR_ALERT: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Alert: Dataset Queue pending tasks exceeded the limit",
|
||||
template_path="queue_monitor_alert_email_template_en-US.html",
|
||||
branded_template_path="queue_monitor_alert_email_template_en-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="警报:数据集队列待处理任务超过限制",
|
||||
template_path="queue_monitor_alert_email_template_zh-CN.html",
|
||||
branded_template_path="queue_monitor_alert_email_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
EmailType.DOCUMENT_CLEAN_NOTIFY: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Dify Knowledge base auto disable notification",
|
||||
template_path="clean_document_job_mail_template-US.html",
|
||||
branded_template_path="clean_document_job_mail_template-US.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="Dify 知识库自动禁用通知",
|
||||
template_path="clean_document_job_mail_template_zh-CN.html",
|
||||
branded_template_path="clean_document_job_mail_template_zh-CN.html",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
return EmailI18nConfig(templates=templates)
|
||||
|
||||
|
||||
# Singleton instance for application-wide use
|
||||
def get_default_email_i18n_service() -> EmailI18nService:
|
||||
"""Get configured email i18n service with default dependencies."""
|
||||
config = create_default_email_config()
|
||||
renderer = FlaskEmailRenderer()
|
||||
branding_service = FeatureBrandingService()
|
||||
sender = FlaskMailSender()
|
||||
|
||||
return EmailI18nService(
|
||||
config=config,
|
||||
renderer=renderer,
|
||||
branding_service=branding_service,
|
||||
sender=sender,
|
||||
)
|
||||
|
||||
|
||||
# Global instance
|
||||
_email_i18n_service: Optional[EmailI18nService] = None
|
||||
|
||||
|
||||
def get_email_i18n_service() -> EmailI18nService:
|
||||
"""Get global email i18n service instance."""
|
||||
global _email_i18n_service
|
||||
if _email_i18n_service is None:
|
||||
_email_i18n_service = get_default_email_i18n_service()
|
||||
return _email_i18n_service
|
@@ -3,12 +3,12 @@ import time
|
||||
from collections import defaultdict
|
||||
|
||||
import click
|
||||
from flask import render_template # type: ignore
|
||||
|
||||
import app
|
||||
from configs import dify_config
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_mail import mail
|
||||
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||
from models.account import Account, Tenant, TenantAccountJoin
|
||||
from models.dataset import Dataset, DatasetAutoDisableLog
|
||||
from services.feature_service import FeatureService
|
||||
@@ -72,14 +72,16 @@ def mail_clean_document_notify_task():
|
||||
document_count = len(document_ids)
|
||||
knowledge_details.append(rf"Knowledge base {dataset.name}: {document_count} documents")
|
||||
if knowledge_details:
|
||||
html_content = render_template(
|
||||
"clean_document_job_mail_template-US.html",
|
||||
userName=account.email,
|
||||
knowledge_details=knowledge_details,
|
||||
url=url,
|
||||
)
|
||||
mail.send(
|
||||
to=account.email, subject="Dify Knowledge base auto disable notification", html=html_content
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_email(
|
||||
email_type=EmailType.DOCUMENT_CLEAN_NOTIFY,
|
||||
language_code="en-US",
|
||||
to=account.email,
|
||||
template_context={
|
||||
"userName": account.email,
|
||||
"knowledge_details": knowledge_details,
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
|
||||
# update notified to True
|
||||
|
@@ -3,13 +3,12 @@ from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import click
|
||||
from flask import render_template
|
||||
from redis import Redis
|
||||
|
||||
import app
|
||||
from configs import dify_config
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_mail import mail
|
||||
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||
|
||||
# Create a dedicated Redis connection (using the same configuration as Celery)
|
||||
celery_broker_url = dify_config.CELERY_BROKER_URL
|
||||
@@ -39,18 +38,20 @@ def queue_monitor_task():
|
||||
alter_emails = dify_config.QUEUE_MONITOR_ALERT_EMAILS
|
||||
if alter_emails:
|
||||
to_list = alter_emails.split(",")
|
||||
email_service = get_email_i18n_service()
|
||||
for to in to_list:
|
||||
try:
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
html_content = render_template(
|
||||
"queue_monitor_alert_email_template_en-US.html",
|
||||
queue_name=queue_name,
|
||||
queue_length=queue_length,
|
||||
threshold=threshold,
|
||||
alert_time=current_time,
|
||||
)
|
||||
mail.send(
|
||||
to=to, subject="Alert: Dataset Queue pending tasks exceeded the limit", html=html_content
|
||||
email_service.send_email(
|
||||
email_type=EmailType.QUEUE_MONITOR_ALERT,
|
||||
language_code="en-US",
|
||||
to=to,
|
||||
template_context={
|
||||
"queue_name": queue_name,
|
||||
"queue_length": queue_length,
|
||||
"threshold": threshold,
|
||||
"alert_time": current_time,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logging.exception(click.style("Exception occurred during sending email", fg="red"))
|
||||
|
@@ -3,14 +3,20 @@ import time
|
||||
|
||||
import click
|
||||
from celery import shared_task # type: ignore
|
||||
from flask import render_template
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_deletion_success_task(to):
|
||||
"""Send email to user regarding account deletion."""
|
||||
def send_deletion_success_task(to: str, language: str = "en-US") -> None:
|
||||
"""
|
||||
Send account deletion success email with internationalization support.
|
||||
|
||||
Args:
|
||||
to: Recipient email address
|
||||
language: Language code for email localization
|
||||
"""
|
||||
if not mail.is_inited():
|
||||
return
|
||||
|
||||
@@ -18,12 +24,16 @@ def send_deletion_success_task(to):
|
||||
start_at = time.perf_counter()
|
||||
|
||||
try:
|
||||
html_content = render_template(
|
||||
"delete_account_success_template_en-US.html",
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_email(
|
||||
email_type=EmailType.ACCOUNT_DELETION_SUCCESS,
|
||||
language_code=language,
|
||||
to=to,
|
||||
email=to,
|
||||
template_context={
|
||||
"to": to,
|
||||
"email": to,
|
||||
},
|
||||
)
|
||||
mail.send(to=to, subject="Your Dify.AI Account Has Been Successfully Deleted", html=html_content)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
@@ -36,12 +46,14 @@ def send_deletion_success_task(to):
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_account_deletion_verification_code(to, code):
|
||||
"""Send email to user regarding account deletion verification code.
|
||||
def send_account_deletion_verification_code(to: str, code: str, language: str = "en-US") -> None:
|
||||
"""
|
||||
Send account deletion verification code email with internationalization support.
|
||||
|
||||
Args:
|
||||
to (str): Recipient email address
|
||||
code (str): Verification code
|
||||
to: Recipient email address
|
||||
code: Verification code
|
||||
language: Language code for email localization
|
||||
"""
|
||||
if not mail.is_inited():
|
||||
return
|
||||
@@ -50,8 +62,16 @@ def send_account_deletion_verification_code(to, code):
|
||||
start_at = time.perf_counter()
|
||||
|
||||
try:
|
||||
html_content = render_template("delete_account_code_email_template_en-US.html", to=to, code=code)
|
||||
mail.send(to=to, subject="Dify.AI Account Deletion and Verification", html=html_content)
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_email(
|
||||
email_type=EmailType.ACCOUNT_DELETION_VERIFICATION,
|
||||
language_code=language,
|
||||
to=to,
|
||||
template_context={
|
||||
"to": to,
|
||||
"code": code,
|
||||
},
|
||||
)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
|
@@ -3,20 +3,21 @@ import time
|
||||
|
||||
import click
|
||||
from celery import shared_task # type: ignore
|
||||
from flask import render_template
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from services.feature_service import FeatureService
|
||||
from libs.email_i18n import get_email_i18n_service
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_change_mail_task(language: str, to: str, code: str, phase: str):
|
||||
def send_change_mail_task(language: str, to: str, code: str, phase: str) -> None:
|
||||
"""
|
||||
Async Send change email mail
|
||||
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
|
||||
:param to: Recipient email address
|
||||
:param code: Change email code
|
||||
:param phase: Change email phase (new_email, old_email)
|
||||
Send change email notification with internationalization support.
|
||||
|
||||
Args:
|
||||
language: Language code for email localization
|
||||
to: Recipient email address
|
||||
code: Email verification code
|
||||
phase: Change email phase ('old_email' or 'new_email')
|
||||
"""
|
||||
if not mail.is_inited():
|
||||
return
|
||||
@@ -24,51 +25,14 @@ def send_change_mail_task(language: str, to: str, code: str, phase: str):
|
||||
logging.info(click.style("Start change email mail to {}".format(to), fg="green"))
|
||||
start_at = time.perf_counter()
|
||||
|
||||
email_config = {
|
||||
"zh-Hans": {
|
||||
"old_email": {
|
||||
"subject": "检测您现在的邮箱",
|
||||
"template_with_brand": "change_mail_confirm_old_template_zh-CN.html",
|
||||
"template_without_brand": "without-brand/change_mail_confirm_old_template_zh-CN.html",
|
||||
},
|
||||
"new_email": {
|
||||
"subject": "确认您的邮箱地址变更",
|
||||
"template_with_brand": "change_mail_confirm_new_template_zh-CN.html",
|
||||
"template_without_brand": "without-brand/change_mail_confirm_new_template_zh-CN.html",
|
||||
},
|
||||
},
|
||||
"en": {
|
||||
"old_email": {
|
||||
"subject": "Check your current email",
|
||||
"template_with_brand": "change_mail_confirm_old_template_en-US.html",
|
||||
"template_without_brand": "without-brand/change_mail_confirm_old_template_en-US.html",
|
||||
},
|
||||
"new_email": {
|
||||
"subject": "Confirm your new email address",
|
||||
"template_with_brand": "change_mail_confirm_new_template_en-US.html",
|
||||
"template_without_brand": "without-brand/change_mail_confirm_new_template_en-US.html",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# send change email mail using different languages
|
||||
try:
|
||||
system_features = FeatureService.get_system_features()
|
||||
lang_key = "zh-Hans" if language == "zh-Hans" else "en"
|
||||
|
||||
if phase not in ["old_email", "new_email"]:
|
||||
raise ValueError("Invalid phase")
|
||||
|
||||
config = email_config[lang_key][phase]
|
||||
subject = config["subject"]
|
||||
|
||||
if system_features.branding.enabled:
|
||||
template = config["template_without_brand"]
|
||||
else:
|
||||
template = config["template_with_brand"]
|
||||
|
||||
html_content = render_template(template, to=to, code=code)
|
||||
mail.send(to=to, subject=subject, html=html_content)
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_change_email(
|
||||
language_code=language,
|
||||
to=to,
|
||||
code=code,
|
||||
phase=phase,
|
||||
)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
|
@@ -3,19 +3,20 @@ import time
|
||||
|
||||
import click
|
||||
from celery import shared_task # type: ignore
|
||||
from flask import render_template
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from services.feature_service import FeatureService
|
||||
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_email_code_login_mail_task(language: str, to: str, code: str):
|
||||
def send_email_code_login_mail_task(language: str, to: str, code: str) -> None:
|
||||
"""
|
||||
Async Send email code login mail
|
||||
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
|
||||
:param to: Recipient email address
|
||||
:param code: Email code to be included in the email
|
||||
Send email code login email with internationalization support.
|
||||
|
||||
Args:
|
||||
language: Language code for email localization
|
||||
to: Recipient email address
|
||||
code: Email verification code
|
||||
"""
|
||||
if not mail.is_inited():
|
||||
return
|
||||
@@ -23,28 +24,17 @@ def send_email_code_login_mail_task(language: str, to: str, code: str):
|
||||
logging.info(click.style("Start email code login mail to {}".format(to), fg="green"))
|
||||
start_at = time.perf_counter()
|
||||
|
||||
# send email code login mail using different languages
|
||||
try:
|
||||
if language == "zh-Hans":
|
||||
template = "email_code_login_mail_template_zh-CN.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
application_title = system_features.branding.application_title
|
||||
template = "without-brand/email_code_login_mail_template_zh-CN.html"
|
||||
html_content = render_template(template, to=to, code=code, application_title=application_title)
|
||||
else:
|
||||
html_content = render_template(template, to=to, code=code)
|
||||
mail.send(to=to, subject="邮箱验证码", html=html_content)
|
||||
else:
|
||||
template = "email_code_login_mail_template_en-US.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
application_title = system_features.branding.application_title
|
||||
template = "without-brand/email_code_login_mail_template_en-US.html"
|
||||
html_content = render_template(template, to=to, code=code, application_title=application_title)
|
||||
else:
|
||||
html_content = render_template(template, to=to, code=code)
|
||||
mail.send(to=to, subject="Email Code", html=html_content)
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_email(
|
||||
email_type=EmailType.EMAIL_CODE_LOGIN,
|
||||
language_code=language,
|
||||
to=to,
|
||||
template_context={
|
||||
"to": to,
|
||||
"code": code,
|
||||
},
|
||||
)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
|
@@ -1,15 +1,17 @@
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
|
||||
import click
|
||||
from celery import shared_task # type: ignore
|
||||
from flask import render_template_string
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from libs.email_i18n import get_email_i18n_service
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_enterprise_email_task(to, subject, body, substitutions):
|
||||
def send_enterprise_email_task(to: list[str], subject: str, body: str, substitutions: Mapping[str, str]):
|
||||
if not mail.is_inited():
|
||||
return
|
||||
|
||||
@@ -19,11 +21,8 @@ def send_enterprise_email_task(to, subject, body, substitutions):
|
||||
try:
|
||||
html_content = render_template_string(body, **substitutions)
|
||||
|
||||
if isinstance(to, list):
|
||||
for t in to:
|
||||
mail.send(to=t, subject=subject, html=html_content)
|
||||
else:
|
||||
mail.send(to=to, subject=subject, html=html_content)
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_raw_email(to=to, subject=subject, html_content=html_content)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
|
@@ -3,24 +3,23 @@ import time
|
||||
|
||||
import click
|
||||
from celery import shared_task # type: ignore
|
||||
from flask import render_template
|
||||
|
||||
from configs import dify_config
|
||||
from extensions.ext_mail import mail
|
||||
from services.feature_service import FeatureService
|
||||
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_invite_member_mail_task(language: str, to: str, token: str, inviter_name: str, workspace_name: str):
|
||||
def send_invite_member_mail_task(language: str, to: str, token: str, inviter_name: str, workspace_name: str) -> None:
|
||||
"""
|
||||
Async Send invite member mail
|
||||
:param language
|
||||
:param to
|
||||
:param token
|
||||
:param inviter_name
|
||||
:param workspace_name
|
||||
Send invite member email with internationalization support.
|
||||
|
||||
Usage: send_invite_member_mail_task.delay(language, to, token, inviter_name, workspace_name)
|
||||
Args:
|
||||
language: Language code for email localization
|
||||
to: Recipient email address
|
||||
token: Invitation token
|
||||
inviter_name: Name of the person sending the invitation
|
||||
workspace_name: Name of the workspace
|
||||
"""
|
||||
if not mail.is_inited():
|
||||
return
|
||||
@@ -30,49 +29,20 @@ def send_invite_member_mail_task(language: str, to: str, token: str, inviter_nam
|
||||
)
|
||||
start_at = time.perf_counter()
|
||||
|
||||
# send invite member mail using different languages
|
||||
try:
|
||||
url = f"{dify_config.CONSOLE_WEB_URL}/activate?token={token}"
|
||||
if language == "zh-Hans":
|
||||
template = "invite_member_mail_template_zh-CN.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
application_title = system_features.branding.application_title
|
||||
template = "without-brand/invite_member_mail_template_zh-CN.html"
|
||||
html_content = render_template(
|
||||
template,
|
||||
to=to,
|
||||
inviter_name=inviter_name,
|
||||
workspace_name=workspace_name,
|
||||
url=url,
|
||||
application_title=application_title,
|
||||
)
|
||||
mail.send(to=to, subject=f"立即加入 {application_title} 工作空间", html=html_content)
|
||||
else:
|
||||
html_content = render_template(
|
||||
template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url
|
||||
)
|
||||
mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content)
|
||||
else:
|
||||
template = "invite_member_mail_template_en-US.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
application_title = system_features.branding.application_title
|
||||
template = "without-brand/invite_member_mail_template_en-US.html"
|
||||
html_content = render_template(
|
||||
template,
|
||||
to=to,
|
||||
inviter_name=inviter_name,
|
||||
workspace_name=workspace_name,
|
||||
url=url,
|
||||
application_title=application_title,
|
||||
)
|
||||
mail.send(to=to, subject=f"Join {application_title} Workspace Now", html=html_content)
|
||||
else:
|
||||
html_content = render_template(
|
||||
template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url
|
||||
)
|
||||
mail.send(to=to, subject="Join Dify Workspace Now", html=html_content)
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_email(
|
||||
email_type=EmailType.INVITE_MEMBER,
|
||||
language_code=language,
|
||||
to=to,
|
||||
template_context={
|
||||
"to": to,
|
||||
"inviter_name": inviter_name,
|
||||
"workspace_name": workspace_name,
|
||||
"url": url,
|
||||
},
|
||||
)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
|
@@ -3,47 +3,40 @@ import time
|
||||
|
||||
import click
|
||||
from celery import shared_task # type: ignore
|
||||
from flask import render_template
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from services.feature_service import FeatureService
|
||||
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspace: str):
|
||||
def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspace: str) -> None:
|
||||
"""
|
||||
Async Send owner transfer confirm mail
|
||||
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
|
||||
:param to: Recipient email address
|
||||
:param workspace: Workspace name
|
||||
Send owner transfer confirmation email with internationalization support.
|
||||
|
||||
Args:
|
||||
language: Language code for email localization
|
||||
to: Recipient email address
|
||||
code: Verification code
|
||||
workspace: Workspace name
|
||||
"""
|
||||
if not mail.is_inited():
|
||||
return
|
||||
|
||||
logging.info(click.style("Start change email mail to {}".format(to), fg="green"))
|
||||
logging.info(click.style("Start owner transfer confirm mail to {}".format(to), fg="green"))
|
||||
start_at = time.perf_counter()
|
||||
# send change email mail using different languages
|
||||
|
||||
try:
|
||||
if language == "zh-Hans":
|
||||
template = "transfer_workspace_owner_confirm_template_zh-CN.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
template = "without-brand/transfer_workspace_owner_confirm_template_zh-CN.html"
|
||||
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace)
|
||||
mail.send(to=to, subject="验证您转移工作空间所有权的请求", html=html_content)
|
||||
else:
|
||||
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace)
|
||||
mail.send(to=to, subject="验证您转移工作空间所有权的请求", html=html_content)
|
||||
else:
|
||||
template = "transfer_workspace_owner_confirm_template_en-US.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
template = "without-brand/transfer_workspace_owner_confirm_template_en-US.html"
|
||||
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace)
|
||||
mail.send(to=to, subject="Verify Your Request to Transfer Workspace Ownership", html=html_content)
|
||||
else:
|
||||
html_content = render_template(template, to=to, code=code, WorkspaceName=workspace)
|
||||
mail.send(to=to, subject="Verify Your Request to Transfer Workspace Ownership", html=html_content)
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_email(
|
||||
email_type=EmailType.OWNER_TRANSFER_CONFIRM,
|
||||
language_code=language,
|
||||
to=to,
|
||||
template_context={
|
||||
"to": to,
|
||||
"code": code,
|
||||
"WorkspaceName": workspace,
|
||||
},
|
||||
)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
@@ -57,96 +50,80 @@ def send_owner_transfer_confirm_task(language: str, to: str, code: str, workspac
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_old_owner_transfer_notify_email_task(language: str, to: str, workspace: str, new_owner_email: str):
|
||||
def send_old_owner_transfer_notify_email_task(language: str, to: str, workspace: str, new_owner_email: str) -> None:
|
||||
"""
|
||||
Async Send owner transfer confirm mail
|
||||
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
|
||||
:param to: Recipient email address
|
||||
:param workspace: Workspace name
|
||||
:param new_owner_email: New owner email
|
||||
Send old owner transfer notification email with internationalization support.
|
||||
|
||||
Args:
|
||||
language: Language code for email localization
|
||||
to: Recipient email address
|
||||
workspace: Workspace name
|
||||
new_owner_email: New owner email address
|
||||
"""
|
||||
if not mail.is_inited():
|
||||
return
|
||||
|
||||
logging.info(click.style("Start change email mail to {}".format(to), fg="green"))
|
||||
logging.info(click.style("Start old owner transfer notify mail to {}".format(to), fg="green"))
|
||||
start_at = time.perf_counter()
|
||||
# send change email mail using different languages
|
||||
|
||||
try:
|
||||
if language == "zh-Hans":
|
||||
template = "transfer_workspace_old_owner_notify_template_zh-CN.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
template = "without-brand/transfer_workspace_old_owner_notify_template_zh-CN.html"
|
||||
html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email)
|
||||
mail.send(to=to, subject="工作区所有权已转移", html=html_content)
|
||||
else:
|
||||
html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email)
|
||||
mail.send(to=to, subject="工作区所有权已转移", html=html_content)
|
||||
else:
|
||||
template = "transfer_workspace_old_owner_notify_template_en-US.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
template = "without-brand/transfer_workspace_old_owner_notify_template_en-US.html"
|
||||
html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email)
|
||||
mail.send(to=to, subject="Workspace ownership has been transferred", html=html_content)
|
||||
else:
|
||||
html_content = render_template(template, to=to, WorkspaceName=workspace, NewOwnerEmail=new_owner_email)
|
||||
mail.send(to=to, subject="Workspace ownership has been transferred", html=html_content)
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_email(
|
||||
email_type=EmailType.OWNER_TRANSFER_OLD_NOTIFY,
|
||||
language_code=language,
|
||||
to=to,
|
||||
template_context={
|
||||
"to": to,
|
||||
"WorkspaceName": workspace,
|
||||
"NewOwnerEmail": new_owner_email,
|
||||
},
|
||||
)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
click.style(
|
||||
"Send owner transfer confirm mail to {} succeeded: latency: {}".format(to, end_at - start_at),
|
||||
"Send old owner transfer notify mail to {} succeeded: latency: {}".format(to, end_at - start_at),
|
||||
fg="green",
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logging.exception("owner transfer confirm email mail to {} failed".format(to))
|
||||
logging.exception("old owner transfer notify email mail to {} failed".format(to))
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_new_owner_transfer_notify_email_task(language: str, to: str, workspace: str):
|
||||
def send_new_owner_transfer_notify_email_task(language: str, to: str, workspace: str) -> None:
|
||||
"""
|
||||
Async Send owner transfer confirm mail
|
||||
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
|
||||
:param to: Recipient email address
|
||||
:param code: Change email code
|
||||
:param workspace: Workspace name
|
||||
Send new owner transfer notification email with internationalization support.
|
||||
|
||||
Args:
|
||||
language: Language code for email localization
|
||||
to: Recipient email address
|
||||
workspace: Workspace name
|
||||
"""
|
||||
if not mail.is_inited():
|
||||
return
|
||||
|
||||
logging.info(click.style("Start change email mail to {}".format(to), fg="green"))
|
||||
logging.info(click.style("Start new owner transfer notify mail to {}".format(to), fg="green"))
|
||||
start_at = time.perf_counter()
|
||||
# send change email mail using different languages
|
||||
|
||||
try:
|
||||
if language == "zh-Hans":
|
||||
template = "transfer_workspace_new_owner_notify_template_zh-CN.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
template = "without-brand/transfer_workspace_new_owner_notify_template_zh-CN.html"
|
||||
html_content = render_template(template, to=to, WorkspaceName=workspace)
|
||||
mail.send(to=to, subject=f"您现在是 {workspace} 的所有者", html=html_content)
|
||||
else:
|
||||
html_content = render_template(template, to=to, WorkspaceName=workspace)
|
||||
mail.send(to=to, subject=f"您现在是 {workspace} 的所有者", html=html_content)
|
||||
else:
|
||||
template = "transfer_workspace_new_owner_notify_template_en-US.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
template = "without-brand/transfer_workspace_new_owner_notify_template_en-US.html"
|
||||
html_content = render_template(template, to=to, WorkspaceName=workspace)
|
||||
mail.send(to=to, subject=f"You are now the owner of {workspace}", html=html_content)
|
||||
else:
|
||||
html_content = render_template(template, to=to, WorkspaceName=workspace)
|
||||
mail.send(to=to, subject=f"You are now the owner of {workspace}", html=html_content)
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_email(
|
||||
email_type=EmailType.OWNER_TRANSFER_NEW_NOTIFY,
|
||||
language_code=language,
|
||||
to=to,
|
||||
template_context={
|
||||
"to": to,
|
||||
"WorkspaceName": workspace,
|
||||
},
|
||||
)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
click.style(
|
||||
"Send owner transfer confirm mail to {} succeeded: latency: {}".format(to, end_at - start_at),
|
||||
"Send new owner transfer notify mail to {} succeeded: latency: {}".format(to, end_at - start_at),
|
||||
fg="green",
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logging.exception("owner transfer confirm email mail to {} failed".format(to))
|
||||
logging.exception("new owner transfer notify email mail to {} failed".format(to))
|
||||
|
@@ -3,19 +3,20 @@ import time
|
||||
|
||||
import click
|
||||
from celery import shared_task # type: ignore
|
||||
from flask import render_template
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from services.feature_service import FeatureService
|
||||
from libs.email_i18n import EmailType, get_email_i18n_service
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
def send_reset_password_mail_task(language: str, to: str, code: str):
|
||||
def send_reset_password_mail_task(language: str, to: str, code: str) -> None:
|
||||
"""
|
||||
Async Send reset password mail
|
||||
:param language: Language in which the email should be sent (e.g., 'en', 'zh')
|
||||
:param to: Recipient email address
|
||||
:param code: Reset password code
|
||||
Send reset password email with internationalization support.
|
||||
|
||||
Args:
|
||||
language: Language code for email localization
|
||||
to: Recipient email address
|
||||
code: Reset password code
|
||||
"""
|
||||
if not mail.is_inited():
|
||||
return
|
||||
@@ -23,30 +24,17 @@ def send_reset_password_mail_task(language: str, to: str, code: str):
|
||||
logging.info(click.style("Start password reset mail to {}".format(to), fg="green"))
|
||||
start_at = time.perf_counter()
|
||||
|
||||
# send reset password mail using different languages
|
||||
try:
|
||||
if language == "zh-Hans":
|
||||
template = "reset_password_mail_template_zh-CN.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
application_title = system_features.branding.application_title
|
||||
template = "without-brand/reset_password_mail_template_zh-CN.html"
|
||||
html_content = render_template(template, to=to, code=code, application_title=application_title)
|
||||
mail.send(to=to, subject=f"设置您的 {application_title} 密码", html=html_content)
|
||||
else:
|
||||
html_content = render_template(template, to=to, code=code)
|
||||
mail.send(to=to, subject="设置您的 Dify 密码", html=html_content)
|
||||
else:
|
||||
template = "reset_password_mail_template_en-US.html"
|
||||
system_features = FeatureService.get_system_features()
|
||||
if system_features.branding.enabled:
|
||||
application_title = system_features.branding.application_title
|
||||
template = "without-brand/reset_password_mail_template_en-US.html"
|
||||
html_content = render_template(template, to=to, code=code, application_title=application_title)
|
||||
mail.send(to=to, subject=f"Set Your {application_title} Password", html=html_content)
|
||||
else:
|
||||
html_content = render_template(template, to=to, code=code)
|
||||
mail.send(to=to, subject="Set Your Dify Password", html=html_content)
|
||||
email_service = get_email_i18n_service()
|
||||
email_service.send_email(
|
||||
email_type=EmailType.RESET_PASSWORD,
|
||||
language_code=language,
|
||||
to=to,
|
||||
template_context={
|
||||
"to": to,
|
||||
"code": code,
|
||||
},
|
||||
)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
|
539
api/tests/unit_tests/libs/test_email_i18n.py
Normal file
539
api/tests/unit_tests/libs/test_email_i18n.py
Normal file
@@ -0,0 +1,539 @@
|
||||
"""
|
||||
Unit tests for EmailI18nService
|
||||
|
||||
Tests the email internationalization service with mocked dependencies
|
||||
following Domain-Driven Design principles.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from libs.email_i18n import (
|
||||
EmailI18nConfig,
|
||||
EmailI18nService,
|
||||
EmailLanguage,
|
||||
EmailTemplate,
|
||||
EmailType,
|
||||
FlaskEmailRenderer,
|
||||
FlaskMailSender,
|
||||
create_default_email_config,
|
||||
get_email_i18n_service,
|
||||
)
|
||||
from services.feature_service import BrandingModel
|
||||
|
||||
|
||||
class MockEmailRenderer:
|
||||
"""Mock implementation of EmailRenderer protocol"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.rendered_templates: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
def render_template(self, template_path: str, **context: Any) -> str:
|
||||
"""Mock render_template that returns a formatted string"""
|
||||
self.rendered_templates.append((template_path, context))
|
||||
return f"<html>Rendered {template_path} with {context}</html>"
|
||||
|
||||
|
||||
class MockBrandingService:
|
||||
"""Mock implementation of BrandingService protocol"""
|
||||
|
||||
def __init__(self, enabled: bool = False, application_title: str = "Dify") -> None:
|
||||
self.enabled = enabled
|
||||
self.application_title = application_title
|
||||
|
||||
def get_branding_config(self) -> BrandingModel:
|
||||
"""Return mock branding configuration"""
|
||||
branding_model = MagicMock(spec=BrandingModel)
|
||||
branding_model.enabled = self.enabled
|
||||
branding_model.application_title = self.application_title
|
||||
return branding_model
|
||||
|
||||
|
||||
class MockEmailSender:
|
||||
"""Mock implementation of EmailSender protocol"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sent_emails: list[dict[str, str]] = []
|
||||
|
||||
def send_email(self, to: str, subject: str, html_content: str) -> None:
|
||||
"""Mock send_email that records sent emails"""
|
||||
self.sent_emails.append(
|
||||
{
|
||||
"to": to,
|
||||
"subject": subject,
|
||||
"html_content": html_content,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TestEmailI18nService:
|
||||
"""Test cases for EmailI18nService"""
|
||||
|
||||
@pytest.fixture
|
||||
def email_config(self) -> EmailI18nConfig:
|
||||
"""Create test email configuration"""
|
||||
return EmailI18nConfig(
|
||||
templates={
|
||||
EmailType.RESET_PASSWORD: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Reset Your {application_title} Password",
|
||||
template_path="reset_password_en.html",
|
||||
branded_template_path="branded/reset_password_en.html",
|
||||
),
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="重置您的 {application_title} 密码",
|
||||
template_path="reset_password_zh.html",
|
||||
branded_template_path="branded/reset_password_zh.html",
|
||||
),
|
||||
},
|
||||
EmailType.INVITE_MEMBER: {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Join {application_title} Workspace",
|
||||
template_path="invite_member_en.html",
|
||||
branded_template_path="branded/invite_member_en.html",
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_renderer(self) -> MockEmailRenderer:
|
||||
"""Create mock email renderer"""
|
||||
return MockEmailRenderer()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_branding_service(self) -> MockBrandingService:
|
||||
"""Create mock branding service"""
|
||||
return MockBrandingService()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sender(self) -> MockEmailSender:
|
||||
"""Create mock email sender"""
|
||||
return MockEmailSender()
|
||||
|
||||
@pytest.fixture
|
||||
def email_service(
|
||||
self,
|
||||
email_config: EmailI18nConfig,
|
||||
mock_renderer: MockEmailRenderer,
|
||||
mock_branding_service: MockBrandingService,
|
||||
mock_sender: MockEmailSender,
|
||||
) -> EmailI18nService:
|
||||
"""Create EmailI18nService with mocked dependencies"""
|
||||
return EmailI18nService(
|
||||
config=email_config,
|
||||
renderer=mock_renderer,
|
||||
branding_service=mock_branding_service,
|
||||
sender=mock_sender,
|
||||
)
|
||||
|
||||
def test_send_email_with_english_language(
|
||||
self,
|
||||
email_service: EmailI18nService,
|
||||
mock_renderer: MockEmailRenderer,
|
||||
mock_sender: MockEmailSender,
|
||||
) -> None:
|
||||
"""Test sending email with English language"""
|
||||
email_service.send_email(
|
||||
email_type=EmailType.RESET_PASSWORD,
|
||||
language_code="en-US",
|
||||
to="test@example.com",
|
||||
template_context={"reset_link": "https://example.com/reset"},
|
||||
)
|
||||
|
||||
# Verify renderer was called with correct template
|
||||
assert len(mock_renderer.rendered_templates) == 1
|
||||
template_path, context = mock_renderer.rendered_templates[0]
|
||||
assert template_path == "reset_password_en.html"
|
||||
assert context["reset_link"] == "https://example.com/reset"
|
||||
assert context["branding_enabled"] is False
|
||||
assert context["application_title"] == "Dify"
|
||||
|
||||
# Verify email was sent
|
||||
assert len(mock_sender.sent_emails) == 1
|
||||
sent_email = mock_sender.sent_emails[0]
|
||||
assert sent_email["to"] == "test@example.com"
|
||||
assert sent_email["subject"] == "Reset Your Dify Password"
|
||||
assert "reset_password_en.html" in sent_email["html_content"]
|
||||
|
||||
def test_send_email_with_chinese_language(
|
||||
self,
|
||||
email_service: EmailI18nService,
|
||||
mock_sender: MockEmailSender,
|
||||
) -> None:
|
||||
"""Test sending email with Chinese language"""
|
||||
email_service.send_email(
|
||||
email_type=EmailType.RESET_PASSWORD,
|
||||
language_code="zh-Hans",
|
||||
to="test@example.com",
|
||||
template_context={"reset_link": "https://example.com/reset"},
|
||||
)
|
||||
|
||||
# Verify email was sent with Chinese subject
|
||||
assert len(mock_sender.sent_emails) == 1
|
||||
sent_email = mock_sender.sent_emails[0]
|
||||
assert sent_email["subject"] == "重置您的 Dify 密码"
|
||||
|
||||
def test_send_email_with_branding_enabled(
|
||||
self,
|
||||
email_config: EmailI18nConfig,
|
||||
mock_renderer: MockEmailRenderer,
|
||||
mock_sender: MockEmailSender,
|
||||
) -> None:
|
||||
"""Test sending email with branding enabled"""
|
||||
# Create branding service with branding enabled
|
||||
branding_service = MockBrandingService(enabled=True, application_title="MyApp")
|
||||
|
||||
email_service = EmailI18nService(
|
||||
config=email_config,
|
||||
renderer=mock_renderer,
|
||||
branding_service=branding_service,
|
||||
sender=mock_sender,
|
||||
)
|
||||
|
||||
email_service.send_email(
|
||||
email_type=EmailType.RESET_PASSWORD,
|
||||
language_code="en-US",
|
||||
to="test@example.com",
|
||||
)
|
||||
|
||||
# Verify branded template was used
|
||||
assert len(mock_renderer.rendered_templates) == 1
|
||||
template_path, context = mock_renderer.rendered_templates[0]
|
||||
assert template_path == "branded/reset_password_en.html"
|
||||
assert context["branding_enabled"] is True
|
||||
assert context["application_title"] == "MyApp"
|
||||
|
||||
# Verify subject includes custom application title
|
||||
assert len(mock_sender.sent_emails) == 1
|
||||
sent_email = mock_sender.sent_emails[0]
|
||||
assert sent_email["subject"] == "Reset Your MyApp Password"
|
||||
|
||||
def test_send_email_with_language_fallback(
|
||||
self,
|
||||
email_service: EmailI18nService,
|
||||
mock_sender: MockEmailSender,
|
||||
) -> None:
|
||||
"""Test language fallback to English when requested language not available"""
|
||||
# Request invite member in Chinese (not configured)
|
||||
email_service.send_email(
|
||||
email_type=EmailType.INVITE_MEMBER,
|
||||
language_code="zh-Hans",
|
||||
to="test@example.com",
|
||||
)
|
||||
|
||||
# Should fall back to English
|
||||
assert len(mock_sender.sent_emails) == 1
|
||||
sent_email = mock_sender.sent_emails[0]
|
||||
assert sent_email["subject"] == "Join Dify Workspace"
|
||||
|
||||
def test_send_email_with_unknown_language_code(
|
||||
self,
|
||||
email_service: EmailI18nService,
|
||||
mock_sender: MockEmailSender,
|
||||
) -> None:
|
||||
"""Test unknown language code falls back to English"""
|
||||
email_service.send_email(
|
||||
email_type=EmailType.RESET_PASSWORD,
|
||||
language_code="fr-FR", # French not configured
|
||||
to="test@example.com",
|
||||
)
|
||||
|
||||
# Should use English
|
||||
assert len(mock_sender.sent_emails) == 1
|
||||
sent_email = mock_sender.sent_emails[0]
|
||||
assert sent_email["subject"] == "Reset Your Dify Password"
|
||||
|
||||
def test_send_change_email_old_phase(
|
||||
self,
|
||||
email_config: EmailI18nConfig,
|
||||
mock_renderer: MockEmailRenderer,
|
||||
mock_sender: MockEmailSender,
|
||||
mock_branding_service: MockBrandingService,
|
||||
) -> None:
|
||||
"""Test sending change email for old email verification"""
|
||||
# Add change email templates to config
|
||||
email_config.templates[EmailType.CHANGE_EMAIL_OLD] = {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Verify your current email",
|
||||
template_path="change_email_old_en.html",
|
||||
branded_template_path="branded/change_email_old_en.html",
|
||||
),
|
||||
}
|
||||
|
||||
email_service = EmailI18nService(
|
||||
config=email_config,
|
||||
renderer=mock_renderer,
|
||||
branding_service=mock_branding_service,
|
||||
sender=mock_sender,
|
||||
)
|
||||
|
||||
email_service.send_change_email(
|
||||
language_code="en-US",
|
||||
to="old@example.com",
|
||||
code="123456",
|
||||
phase="old_email",
|
||||
)
|
||||
|
||||
# Verify correct template and context
|
||||
assert len(mock_renderer.rendered_templates) == 1
|
||||
template_path, context = mock_renderer.rendered_templates[0]
|
||||
assert template_path == "change_email_old_en.html"
|
||||
assert context["to"] == "old@example.com"
|
||||
assert context["code"] == "123456"
|
||||
|
||||
def test_send_change_email_new_phase(
|
||||
self,
|
||||
email_config: EmailI18nConfig,
|
||||
mock_renderer: MockEmailRenderer,
|
||||
mock_sender: MockEmailSender,
|
||||
mock_branding_service: MockBrandingService,
|
||||
) -> None:
|
||||
"""Test sending change email for new email verification"""
|
||||
# Add change email templates to config
|
||||
email_config.templates[EmailType.CHANGE_EMAIL_NEW] = {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="Verify your new email",
|
||||
template_path="change_email_new_en.html",
|
||||
branded_template_path="branded/change_email_new_en.html",
|
||||
),
|
||||
}
|
||||
|
||||
email_service = EmailI18nService(
|
||||
config=email_config,
|
||||
renderer=mock_renderer,
|
||||
branding_service=mock_branding_service,
|
||||
sender=mock_sender,
|
||||
)
|
||||
|
||||
email_service.send_change_email(
|
||||
language_code="en-US",
|
||||
to="new@example.com",
|
||||
code="654321",
|
||||
phase="new_email",
|
||||
)
|
||||
|
||||
# Verify correct template and context
|
||||
assert len(mock_renderer.rendered_templates) == 1
|
||||
template_path, context = mock_renderer.rendered_templates[0]
|
||||
assert template_path == "change_email_new_en.html"
|
||||
assert context["to"] == "new@example.com"
|
||||
assert context["code"] == "654321"
|
||||
|
||||
def test_send_change_email_invalid_phase(
|
||||
self,
|
||||
email_service: EmailI18nService,
|
||||
) -> None:
|
||||
"""Test sending change email with invalid phase raises error"""
|
||||
with pytest.raises(ValueError, match="Invalid phase: invalid_phase"):
|
||||
email_service.send_change_email(
|
||||
language_code="en-US",
|
||||
to="test@example.com",
|
||||
code="123456",
|
||||
phase="invalid_phase",
|
||||
)
|
||||
|
||||
def test_send_raw_email_single_recipient(
|
||||
self,
|
||||
email_service: EmailI18nService,
|
||||
mock_sender: MockEmailSender,
|
||||
) -> None:
|
||||
"""Test sending raw email to single recipient"""
|
||||
email_service.send_raw_email(
|
||||
to="test@example.com",
|
||||
subject="Test Subject",
|
||||
html_content="<html>Test Content</html>",
|
||||
)
|
||||
|
||||
assert len(mock_sender.sent_emails) == 1
|
||||
sent_email = mock_sender.sent_emails[0]
|
||||
assert sent_email["to"] == "test@example.com"
|
||||
assert sent_email["subject"] == "Test Subject"
|
||||
assert sent_email["html_content"] == "<html>Test Content</html>"
|
||||
|
||||
def test_send_raw_email_multiple_recipients(
|
||||
self,
|
||||
email_service: EmailI18nService,
|
||||
mock_sender: MockEmailSender,
|
||||
) -> None:
|
||||
"""Test sending raw email to multiple recipients"""
|
||||
recipients = ["user1@example.com", "user2@example.com", "user3@example.com"]
|
||||
|
||||
email_service.send_raw_email(
|
||||
to=recipients,
|
||||
subject="Test Subject",
|
||||
html_content="<html>Test Content</html>",
|
||||
)
|
||||
|
||||
# Should send individual emails to each recipient
|
||||
assert len(mock_sender.sent_emails) == 3
|
||||
for i, recipient in enumerate(recipients):
|
||||
sent_email = mock_sender.sent_emails[i]
|
||||
assert sent_email["to"] == recipient
|
||||
assert sent_email["subject"] == "Test Subject"
|
||||
assert sent_email["html_content"] == "<html>Test Content</html>"
|
||||
|
||||
def test_get_template_missing_email_type(
|
||||
self,
|
||||
email_config: EmailI18nConfig,
|
||||
) -> None:
|
||||
"""Test getting template for missing email type raises error"""
|
||||
with pytest.raises(ValueError, match="No templates configured for email type"):
|
||||
email_config.get_template(EmailType.EMAIL_CODE_LOGIN, EmailLanguage.EN_US)
|
||||
|
||||
def test_get_template_missing_language_and_english(
|
||||
self,
|
||||
email_config: EmailI18nConfig,
|
||||
) -> None:
|
||||
"""Test error when neither requested language nor English fallback exists"""
|
||||
# Add template without English fallback
|
||||
email_config.templates[EmailType.EMAIL_CODE_LOGIN] = {
|
||||
EmailLanguage.ZH_HANS: EmailTemplate(
|
||||
subject="Test",
|
||||
template_path="test.html",
|
||||
branded_template_path="branded/test.html",
|
||||
),
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="No template found for"):
|
||||
# Request a language that doesn't exist and no English fallback
|
||||
email_config.get_template(EmailType.EMAIL_CODE_LOGIN, EmailLanguage.EN_US)
|
||||
|
||||
def test_subject_templating_with_variables(
|
||||
self,
|
||||
email_config: EmailI18nConfig,
|
||||
mock_renderer: MockEmailRenderer,
|
||||
mock_sender: MockEmailSender,
|
||||
mock_branding_service: MockBrandingService,
|
||||
) -> None:
|
||||
"""Test subject templating with custom variables"""
|
||||
# Add template with variable in subject
|
||||
email_config.templates[EmailType.OWNER_TRANSFER_NEW_NOTIFY] = {
|
||||
EmailLanguage.EN_US: EmailTemplate(
|
||||
subject="You are now the owner of {WorkspaceName}",
|
||||
template_path="owner_transfer_en.html",
|
||||
branded_template_path="branded/owner_transfer_en.html",
|
||||
),
|
||||
}
|
||||
|
||||
email_service = EmailI18nService(
|
||||
config=email_config,
|
||||
renderer=mock_renderer,
|
||||
branding_service=mock_branding_service,
|
||||
sender=mock_sender,
|
||||
)
|
||||
|
||||
email_service.send_email(
|
||||
email_type=EmailType.OWNER_TRANSFER_NEW_NOTIFY,
|
||||
language_code="en-US",
|
||||
to="test@example.com",
|
||||
template_context={"WorkspaceName": "My Workspace"},
|
||||
)
|
||||
|
||||
# Verify subject was templated correctly
|
||||
assert len(mock_sender.sent_emails) == 1
|
||||
sent_email = mock_sender.sent_emails[0]
|
||||
assert sent_email["subject"] == "You are now the owner of My Workspace"
|
||||
|
||||
def test_email_language_from_language_code(self) -> None:
|
||||
"""Test EmailLanguage.from_language_code method"""
|
||||
assert EmailLanguage.from_language_code("zh-Hans") == EmailLanguage.ZH_HANS
|
||||
assert EmailLanguage.from_language_code("en-US") == EmailLanguage.EN_US
|
||||
assert EmailLanguage.from_language_code("fr-FR") == EmailLanguage.EN_US # Fallback
|
||||
assert EmailLanguage.from_language_code("unknown") == EmailLanguage.EN_US # Fallback
|
||||
|
||||
|
||||
class TestEmailI18nIntegration:
|
||||
"""Integration tests for email i18n components"""
|
||||
|
||||
def test_create_default_email_config(self) -> None:
|
||||
"""Test creating default email configuration"""
|
||||
config = create_default_email_config()
|
||||
|
||||
# Verify key email types have at least English template
|
||||
expected_types = [
|
||||
EmailType.RESET_PASSWORD,
|
||||
EmailType.INVITE_MEMBER,
|
||||
EmailType.EMAIL_CODE_LOGIN,
|
||||
EmailType.CHANGE_EMAIL_OLD,
|
||||
EmailType.CHANGE_EMAIL_NEW,
|
||||
EmailType.OWNER_TRANSFER_CONFIRM,
|
||||
EmailType.OWNER_TRANSFER_OLD_NOTIFY,
|
||||
EmailType.OWNER_TRANSFER_NEW_NOTIFY,
|
||||
EmailType.ACCOUNT_DELETION_SUCCESS,
|
||||
EmailType.ACCOUNT_DELETION_VERIFICATION,
|
||||
EmailType.QUEUE_MONITOR_ALERT,
|
||||
EmailType.DOCUMENT_CLEAN_NOTIFY,
|
||||
]
|
||||
|
||||
for email_type in expected_types:
|
||||
assert email_type in config.templates
|
||||
assert EmailLanguage.EN_US in config.templates[email_type]
|
||||
|
||||
# Verify some have Chinese translations
|
||||
assert EmailLanguage.ZH_HANS in config.templates[EmailType.RESET_PASSWORD]
|
||||
assert EmailLanguage.ZH_HANS in config.templates[EmailType.INVITE_MEMBER]
|
||||
|
||||
def test_get_email_i18n_service(self) -> None:
|
||||
"""Test getting global email i18n service instance"""
|
||||
service1 = get_email_i18n_service()
|
||||
service2 = get_email_i18n_service()
|
||||
|
||||
# Should return the same instance
|
||||
assert service1 is service2
|
||||
|
||||
def test_flask_email_renderer(self) -> None:
|
||||
"""Test FlaskEmailRenderer implementation"""
|
||||
renderer = FlaskEmailRenderer()
|
||||
|
||||
# Should raise TemplateNotFound when template doesn't exist
|
||||
from jinja2.exceptions import TemplateNotFound
|
||||
|
||||
with pytest.raises(TemplateNotFound):
|
||||
renderer.render_template("test.html", foo="bar")
|
||||
|
||||
def test_flask_mail_sender_not_initialized(self) -> None:
|
||||
"""Test FlaskMailSender when mail is not initialized"""
|
||||
sender = FlaskMailSender()
|
||||
|
||||
# Mock mail.is_inited() to return False
|
||||
import libs.email_i18n
|
||||
|
||||
original_mail = libs.email_i18n.mail
|
||||
mock_mail = MagicMock()
|
||||
mock_mail.is_inited.return_value = False
|
||||
libs.email_i18n.mail = mock_mail
|
||||
|
||||
try:
|
||||
# Should not send email when mail is not initialized
|
||||
sender.send_email("test@example.com", "Subject", "<html>Content</html>")
|
||||
mock_mail.send.assert_not_called()
|
||||
finally:
|
||||
# Restore original mail
|
||||
libs.email_i18n.mail = original_mail
|
||||
|
||||
def test_flask_mail_sender_initialized(self) -> None:
|
||||
"""Test FlaskMailSender when mail is initialized"""
|
||||
sender = FlaskMailSender()
|
||||
|
||||
# Mock mail.is_inited() to return True
|
||||
import libs.email_i18n
|
||||
|
||||
original_mail = libs.email_i18n.mail
|
||||
mock_mail = MagicMock()
|
||||
mock_mail.is_inited.return_value = True
|
||||
libs.email_i18n.mail = mock_mail
|
||||
|
||||
try:
|
||||
# Should send email when mail is initialized
|
||||
sender.send_email("test@example.com", "Subject", "<html>Content</html>")
|
||||
mock_mail.send.assert_called_once_with(
|
||||
to="test@example.com",
|
||||
subject="Subject",
|
||||
html="<html>Content</html>",
|
||||
)
|
||||
finally:
|
||||
# Restore original mail
|
||||
libs.email_i18n.mail = original_mail
|
Reference in New Issue
Block a user