feat: add email settings with database overrides for admin and vendor
Platform Email Settings (Admin): - Add GET/PUT/DELETE /admin/settings/email/* endpoints - Settings stored in admin_settings table override .env values - Support all providers: SMTP, SendGrid, Mailgun, Amazon SES - Edit mode UI with provider-specific configuration forms - Reset to .env defaults functionality - Test email to verify configuration Vendor Email Settings: - Add VendorEmailSettings model with one-to-one vendor relationship - Migration: v0a1b2c3d4e5_add_vendor_email_settings.py - Service: vendor_email_settings_service.py with tier validation - API endpoints: /vendor/email-settings/* (CRUD, status, verify) - Email tab in vendor settings page with full configuration - Warning banner until email is configured (like billing warnings) - Premium providers (SendGrid, Mailgun, SES) tier-gated to Business+ Email Service Updates: - get_platform_email_config(db) checks DB first, then .env - Configurable provider classes accept config dict - EmailService uses database-aware providers - Vendor emails use vendor's own SMTP (Wizamart doesn't pay) - "Powered by Wizamart" footer for Essential/Professional tiers - White-label (no footer) for Business/Enterprise tiers Other: - Add scripts/install.py for first-time platform setup - Add make install target - Update init-prod to include email template seeding 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,20 @@ PLATFORM_SUPPORT_EMAIL = "support@wizamart.com"
|
||||
PLATFORM_DEFAULT_LANGUAGE = "en"
|
||||
SUPPORTED_LANGUAGES = ["en", "fr", "de", "lb"]
|
||||
|
||||
# Tiers that get white-label (no "Powered by Wizamart" footer)
|
||||
WHITELABEL_TIERS = {"business", "enterprise"}
|
||||
|
||||
# Powered by Wizamart footer (added for Essential/Professional tiers)
|
||||
POWERED_BY_FOOTER_HTML = """
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #e5e7eb; text-align: center;">
|
||||
<p style="color: #9ca3af; font-size: 12px; margin: 0;">
|
||||
Powered by <a href="https://wizamart.com" style="color: #6b46c1; text-decoration: none;">Wizamart</a>
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
POWERED_BY_FOOTER_TEXT = "\n\n---\nPowered by Wizamart - https://wizamart.com"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ResolvedTemplate:
|
||||
@@ -340,7 +354,582 @@ class DebugProvider(EmailProvider):
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# EMAIL SERVICE
|
||||
# PLATFORM CONFIG HELPERS (DB overrides .env)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
def get_platform_email_config(db: Session) -> dict:
|
||||
"""
|
||||
Get effective platform email configuration.
|
||||
|
||||
Priority: Database settings > Environment variables (.env)
|
||||
|
||||
Returns:
|
||||
Dictionary with all email configuration values
|
||||
"""
|
||||
from models.database.admin import AdminSetting
|
||||
|
||||
def get_db_setting(key: str) -> str | None:
|
||||
setting = db.query(AdminSetting).filter(AdminSetting.key == key).first()
|
||||
return setting.value if setting else None
|
||||
|
||||
config = {}
|
||||
|
||||
# Provider
|
||||
db_provider = get_db_setting("email_provider")
|
||||
config["provider"] = db_provider if db_provider else settings.email_provider
|
||||
|
||||
# From settings
|
||||
db_from_email = get_db_setting("email_from_address")
|
||||
config["from_email"] = db_from_email if db_from_email else settings.email_from_address
|
||||
|
||||
db_from_name = get_db_setting("email_from_name")
|
||||
config["from_name"] = db_from_name if db_from_name else settings.email_from_name
|
||||
|
||||
db_reply_to = get_db_setting("email_reply_to")
|
||||
config["reply_to"] = db_reply_to if db_reply_to else settings.email_reply_to
|
||||
|
||||
# SMTP settings
|
||||
db_smtp_host = get_db_setting("smtp_host")
|
||||
config["smtp_host"] = db_smtp_host if db_smtp_host else settings.smtp_host
|
||||
|
||||
db_smtp_port = get_db_setting("smtp_port")
|
||||
config["smtp_port"] = int(db_smtp_port) if db_smtp_port else settings.smtp_port
|
||||
|
||||
db_smtp_user = get_db_setting("smtp_user")
|
||||
config["smtp_user"] = db_smtp_user if db_smtp_user else settings.smtp_user
|
||||
|
||||
db_smtp_password = get_db_setting("smtp_password")
|
||||
config["smtp_password"] = db_smtp_password if db_smtp_password else settings.smtp_password
|
||||
|
||||
db_smtp_use_tls = get_db_setting("smtp_use_tls")
|
||||
config["smtp_use_tls"] = db_smtp_use_tls.lower() in ("true", "1", "yes") if db_smtp_use_tls else settings.smtp_use_tls
|
||||
|
||||
db_smtp_use_ssl = get_db_setting("smtp_use_ssl")
|
||||
config["smtp_use_ssl"] = db_smtp_use_ssl.lower() in ("true", "1", "yes") if db_smtp_use_ssl else settings.smtp_use_ssl
|
||||
|
||||
# SendGrid
|
||||
db_sendgrid_key = get_db_setting("sendgrid_api_key")
|
||||
config["sendgrid_api_key"] = db_sendgrid_key if db_sendgrid_key else settings.sendgrid_api_key
|
||||
|
||||
# Mailgun
|
||||
db_mailgun_key = get_db_setting("mailgun_api_key")
|
||||
config["mailgun_api_key"] = db_mailgun_key if db_mailgun_key else settings.mailgun_api_key
|
||||
|
||||
db_mailgun_domain = get_db_setting("mailgun_domain")
|
||||
config["mailgun_domain"] = db_mailgun_domain if db_mailgun_domain else settings.mailgun_domain
|
||||
|
||||
# AWS SES
|
||||
db_aws_key = get_db_setting("aws_access_key_id")
|
||||
config["aws_access_key_id"] = db_aws_key if db_aws_key else settings.aws_access_key_id
|
||||
|
||||
db_aws_secret = get_db_setting("aws_secret_access_key")
|
||||
config["aws_secret_access_key"] = db_aws_secret if db_aws_secret else settings.aws_secret_access_key
|
||||
|
||||
db_aws_region = get_db_setting("aws_region")
|
||||
config["aws_region"] = db_aws_region if db_aws_region else settings.aws_region
|
||||
|
||||
# Behavior
|
||||
db_enabled = get_db_setting("email_enabled")
|
||||
config["enabled"] = db_enabled.lower() in ("true", "1", "yes") if db_enabled else settings.email_enabled
|
||||
|
||||
db_debug = get_db_setting("email_debug")
|
||||
config["debug"] = db_debug.lower() in ("true", "1", "yes") if db_debug else settings.email_debug
|
||||
|
||||
return config
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURABLE PLATFORM PROVIDERS (use config dict instead of global settings)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class ConfigurableSMTPProvider(EmailProvider):
|
||||
"""SMTP provider using config dictionary."""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
|
||||
def send(
|
||||
self,
|
||||
to_email: str,
|
||||
to_name: str | None,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
from_email: str,
|
||||
from_name: str | None,
|
||||
reply_to: str | None = None,
|
||||
) -> tuple[bool, str | None, str | None]:
|
||||
try:
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = f"{from_name} <{from_email}>" if from_name else from_email
|
||||
msg["To"] = f"{to_name} <{to_email}>" if to_name else to_email
|
||||
|
||||
if reply_to:
|
||||
msg["Reply-To"] = reply_to
|
||||
|
||||
if body_text:
|
||||
msg.attach(MIMEText(body_text, "plain", "utf-8"))
|
||||
msg.attach(MIMEText(body_html, "html", "utf-8"))
|
||||
|
||||
if self.config.get("smtp_use_ssl"):
|
||||
server = smtplib.SMTP_SSL(self.config["smtp_host"], self.config["smtp_port"])
|
||||
else:
|
||||
server = smtplib.SMTP(self.config["smtp_host"], self.config["smtp_port"])
|
||||
|
||||
try:
|
||||
if self.config.get("smtp_use_tls") and not self.config.get("smtp_use_ssl"):
|
||||
server.starttls()
|
||||
|
||||
if self.config.get("smtp_user") and self.config.get("smtp_password"):
|
||||
server.login(self.config["smtp_user"], self.config["smtp_password"])
|
||||
|
||||
server.sendmail(from_email, [to_email], msg.as_string())
|
||||
return True, None, None
|
||||
|
||||
finally:
|
||||
server.quit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Configurable SMTP send error: {e}")
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
class ConfigurableSendGridProvider(EmailProvider):
|
||||
"""SendGrid provider using config dictionary."""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
|
||||
def send(
|
||||
self,
|
||||
to_email: str,
|
||||
to_name: str | None,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
from_email: str,
|
||||
from_name: str | None,
|
||||
reply_to: str | None = None,
|
||||
) -> tuple[bool, str | None, str | None]:
|
||||
try:
|
||||
from sendgrid import SendGridAPIClient
|
||||
from sendgrid.helpers.mail import Mail, Email, To, Content
|
||||
|
||||
message = Mail(
|
||||
from_email=Email(from_email, from_name),
|
||||
to_emails=To(to_email, to_name),
|
||||
subject=subject,
|
||||
)
|
||||
|
||||
message.add_content(Content("text/html", body_html))
|
||||
if body_text:
|
||||
message.add_content(Content("text/plain", body_text))
|
||||
|
||||
if reply_to:
|
||||
message.reply_to = Email(reply_to)
|
||||
|
||||
sg = SendGridAPIClient(self.config["sendgrid_api_key"])
|
||||
response = sg.send(message)
|
||||
|
||||
if response.status_code in (200, 201, 202):
|
||||
message_id = response.headers.get("X-Message-Id")
|
||||
return True, message_id, None
|
||||
else:
|
||||
return False, None, f"SendGrid error: {response.status_code}"
|
||||
|
||||
except ImportError:
|
||||
return False, None, "SendGrid library not installed"
|
||||
except Exception as e:
|
||||
logger.error(f"Configurable SendGrid send error: {e}")
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
class ConfigurableMailgunProvider(EmailProvider):
|
||||
"""Mailgun provider using config dictionary."""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
|
||||
def send(
|
||||
self,
|
||||
to_email: str,
|
||||
to_name: str | None,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
from_email: str,
|
||||
from_name: str | None,
|
||||
reply_to: str | None = None,
|
||||
) -> tuple[bool, str | None, str | None]:
|
||||
try:
|
||||
import requests
|
||||
|
||||
from_str = f"{from_name} <{from_email}>" if from_name else from_email
|
||||
to_str = f"{to_name} <{to_email}>" if to_name else to_email
|
||||
|
||||
data = {
|
||||
"from": from_str,
|
||||
"to": to_str,
|
||||
"subject": subject,
|
||||
"html": body_html,
|
||||
}
|
||||
|
||||
if body_text:
|
||||
data["text"] = body_text
|
||||
if reply_to:
|
||||
data["h:Reply-To"] = reply_to
|
||||
|
||||
response = requests.post(
|
||||
f"https://api.mailgun.net/v3/{self.config['mailgun_domain']}/messages",
|
||||
auth=("api", self.config["mailgun_api_key"]),
|
||||
data=data,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return True, result.get("id"), None
|
||||
else:
|
||||
return False, None, f"Mailgun error: {response.status_code} - {response.text}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Configurable Mailgun send error: {e}")
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
class ConfigurableSESProvider(EmailProvider):
|
||||
"""Amazon SES provider using config dictionary."""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
|
||||
def send(
|
||||
self,
|
||||
to_email: str,
|
||||
to_name: str | None,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
from_email: str,
|
||||
from_name: str | None,
|
||||
reply_to: str | None = None,
|
||||
) -> tuple[bool, str | None, str | None]:
|
||||
try:
|
||||
import boto3
|
||||
|
||||
ses = boto3.client(
|
||||
"ses",
|
||||
region_name=self.config["aws_region"],
|
||||
aws_access_key_id=self.config["aws_access_key_id"],
|
||||
aws_secret_access_key=self.config["aws_secret_access_key"],
|
||||
)
|
||||
|
||||
from_str = f"{from_name} <{from_email}>" if from_name else from_email
|
||||
|
||||
body = {"Html": {"Charset": "UTF-8", "Data": body_html}}
|
||||
if body_text:
|
||||
body["Text"] = {"Charset": "UTF-8", "Data": body_text}
|
||||
|
||||
kwargs = {
|
||||
"Source": from_str,
|
||||
"Destination": {"ToAddresses": [to_email]},
|
||||
"Message": {
|
||||
"Subject": {"Charset": "UTF-8", "Data": subject},
|
||||
"Body": body,
|
||||
},
|
||||
}
|
||||
|
||||
if reply_to:
|
||||
kwargs["ReplyToAddresses"] = [reply_to]
|
||||
|
||||
response = ses.send_email(**kwargs)
|
||||
return True, response.get("MessageId"), None
|
||||
|
||||
except ImportError:
|
||||
return False, None, "boto3 library not installed"
|
||||
except Exception as e:
|
||||
logger.error(f"Configurable SES send error: {e}")
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
def get_platform_provider(db: Session) -> EmailProvider:
|
||||
"""
|
||||
Get the configured email provider using effective platform config.
|
||||
|
||||
Uses database settings if available, otherwise falls back to .env.
|
||||
"""
|
||||
config = get_platform_email_config(db)
|
||||
|
||||
if config.get("debug"):
|
||||
return DebugProvider()
|
||||
|
||||
provider_map = {
|
||||
"smtp": ConfigurableSMTPProvider,
|
||||
"sendgrid": ConfigurableSendGridProvider,
|
||||
"mailgun": ConfigurableMailgunProvider,
|
||||
"ses": ConfigurableSESProvider,
|
||||
}
|
||||
|
||||
provider_name = config.get("provider", "smtp").lower()
|
||||
provider_class = provider_map.get(provider_name)
|
||||
|
||||
if not provider_class:
|
||||
logger.warning(f"Unknown email provider: {provider_name}, using SMTP")
|
||||
return ConfigurableSMTPProvider(config)
|
||||
|
||||
return provider_class(config)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# VENDOR EMAIL PROVIDERS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class VendorSMTPProvider(EmailProvider):
|
||||
"""SMTP provider using vendor-specific settings."""
|
||||
|
||||
def __init__(self, vendor_settings):
|
||||
self.settings = vendor_settings
|
||||
|
||||
def send(
|
||||
self,
|
||||
to_email: str,
|
||||
to_name: str | None,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
from_email: str,
|
||||
from_name: str | None,
|
||||
reply_to: str | None = None,
|
||||
) -> tuple[bool, str | None, str | None]:
|
||||
try:
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = f"{from_name} <{from_email}>" if from_name else from_email
|
||||
msg["To"] = f"{to_name} <{to_email}>" if to_name else to_email
|
||||
|
||||
if reply_to:
|
||||
msg["Reply-To"] = reply_to
|
||||
|
||||
if body_text:
|
||||
msg.attach(MIMEText(body_text, "plain", "utf-8"))
|
||||
msg.attach(MIMEText(body_html, "html", "utf-8"))
|
||||
|
||||
# Use vendor's SMTP settings
|
||||
if self.settings.smtp_use_ssl:
|
||||
server = smtplib.SMTP_SSL(self.settings.smtp_host, self.settings.smtp_port)
|
||||
else:
|
||||
server = smtplib.SMTP(self.settings.smtp_host, self.settings.smtp_port)
|
||||
|
||||
try:
|
||||
if self.settings.smtp_use_tls and not self.settings.smtp_use_ssl:
|
||||
server.starttls()
|
||||
|
||||
if self.settings.smtp_username and self.settings.smtp_password:
|
||||
server.login(self.settings.smtp_username, self.settings.smtp_password)
|
||||
|
||||
server.sendmail(from_email, [to_email], msg.as_string())
|
||||
return True, None, None
|
||||
|
||||
finally:
|
||||
server.quit()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Vendor SMTP send error: {e}")
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
class VendorSendGridProvider(EmailProvider):
|
||||
"""SendGrid provider using vendor-specific API key."""
|
||||
|
||||
def __init__(self, vendor_settings):
|
||||
self.settings = vendor_settings
|
||||
|
||||
def send(
|
||||
self,
|
||||
to_email: str,
|
||||
to_name: str | None,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
from_email: str,
|
||||
from_name: str | None,
|
||||
reply_to: str | None = None,
|
||||
) -> tuple[bool, str | None, str | None]:
|
||||
try:
|
||||
from sendgrid import SendGridAPIClient
|
||||
from sendgrid.helpers.mail import Mail, Email, To, Content
|
||||
|
||||
message = Mail(
|
||||
from_email=Email(from_email, from_name),
|
||||
to_emails=To(to_email, to_name),
|
||||
subject=subject,
|
||||
)
|
||||
|
||||
message.add_content(Content("text/html", body_html))
|
||||
if body_text:
|
||||
message.add_content(Content("text/plain", body_text))
|
||||
|
||||
if reply_to:
|
||||
message.reply_to = Email(reply_to)
|
||||
|
||||
sg = SendGridAPIClient(self.settings.sendgrid_api_key)
|
||||
response = sg.send(message)
|
||||
|
||||
if response.status_code in (200, 201, 202):
|
||||
message_id = response.headers.get("X-Message-Id")
|
||||
return True, message_id, None
|
||||
else:
|
||||
return False, None, f"SendGrid error: {response.status_code}"
|
||||
|
||||
except ImportError:
|
||||
return False, None, "SendGrid library not installed"
|
||||
except Exception as e:
|
||||
logger.error(f"Vendor SendGrid send error: {e}")
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
class VendorMailgunProvider(EmailProvider):
|
||||
"""Mailgun provider using vendor-specific settings."""
|
||||
|
||||
def __init__(self, vendor_settings):
|
||||
self.settings = vendor_settings
|
||||
|
||||
def send(
|
||||
self,
|
||||
to_email: str,
|
||||
to_name: str | None,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
from_email: str,
|
||||
from_name: str | None,
|
||||
reply_to: str | None = None,
|
||||
) -> tuple[bool, str | None, str | None]:
|
||||
try:
|
||||
import requests
|
||||
|
||||
from_str = f"{from_name} <{from_email}>" if from_name else from_email
|
||||
to_str = f"{to_name} <{to_email}>" if to_name else to_email
|
||||
|
||||
data = {
|
||||
"from": from_str,
|
||||
"to": to_str,
|
||||
"subject": subject,
|
||||
"html": body_html,
|
||||
}
|
||||
|
||||
if body_text:
|
||||
data["text"] = body_text
|
||||
if reply_to:
|
||||
data["h:Reply-To"] = reply_to
|
||||
|
||||
response = requests.post(
|
||||
f"https://api.mailgun.net/v3/{self.settings.mailgun_domain}/messages",
|
||||
auth=("api", self.settings.mailgun_api_key),
|
||||
data=data,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return True, result.get("id"), None
|
||||
else:
|
||||
return False, None, f"Mailgun error: {response.status_code} - {response.text}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Vendor Mailgun send error: {e}")
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
class VendorSESProvider(EmailProvider):
|
||||
"""Amazon SES provider using vendor-specific credentials."""
|
||||
|
||||
def __init__(self, vendor_settings):
|
||||
self.settings = vendor_settings
|
||||
|
||||
def send(
|
||||
self,
|
||||
to_email: str,
|
||||
to_name: str | None,
|
||||
subject: str,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
from_email: str,
|
||||
from_name: str | None,
|
||||
reply_to: str | None = None,
|
||||
) -> tuple[bool, str | None, str | None]:
|
||||
try:
|
||||
import boto3
|
||||
|
||||
ses = boto3.client(
|
||||
"ses",
|
||||
region_name=self.settings.ses_region,
|
||||
aws_access_key_id=self.settings.ses_access_key_id,
|
||||
aws_secret_access_key=self.settings.ses_secret_access_key,
|
||||
)
|
||||
|
||||
from_str = f"{from_name} <{from_email}>" if from_name else from_email
|
||||
|
||||
body = {"Html": {"Charset": "UTF-8", "Data": body_html}}
|
||||
if body_text:
|
||||
body["Text"] = {"Charset": "UTF-8", "Data": body_text}
|
||||
|
||||
kwargs = {
|
||||
"Source": from_str,
|
||||
"Destination": {"ToAddresses": [to_email]},
|
||||
"Message": {
|
||||
"Subject": {"Charset": "UTF-8", "Data": subject},
|
||||
"Body": body,
|
||||
},
|
||||
}
|
||||
|
||||
if reply_to:
|
||||
kwargs["ReplyToAddresses"] = [reply_to]
|
||||
|
||||
response = ses.send_email(**kwargs)
|
||||
return True, response.get("MessageId"), None
|
||||
|
||||
except ImportError:
|
||||
return False, None, "boto3 library not installed"
|
||||
except Exception as e:
|
||||
logger.error(f"Vendor SES send error: {e}")
|
||||
return False, None, str(e)
|
||||
|
||||
|
||||
def get_vendor_provider(vendor_settings) -> EmailProvider | None:
|
||||
"""
|
||||
Create an email provider instance using vendor's settings.
|
||||
|
||||
Args:
|
||||
vendor_settings: VendorEmailSettings model instance
|
||||
|
||||
Returns:
|
||||
EmailProvider instance or None if not configured
|
||||
"""
|
||||
if not vendor_settings or not vendor_settings.is_configured:
|
||||
return None
|
||||
|
||||
provider_map = {
|
||||
"smtp": VendorSMTPProvider,
|
||||
"sendgrid": VendorSendGridProvider,
|
||||
"mailgun": VendorMailgunProvider,
|
||||
"ses": VendorSESProvider,
|
||||
}
|
||||
|
||||
provider_class = provider_map.get(vendor_settings.provider)
|
||||
if not provider_class:
|
||||
logger.warning(f"Unknown vendor email provider: {vendor_settings.provider}")
|
||||
return None
|
||||
|
||||
return provider_class(vendor_settings)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# PLATFORM EMAIL PROVIDER
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@@ -387,15 +976,24 @@ class EmailService:
|
||||
subject="Hello",
|
||||
body_html="<h1>Hello</h1>",
|
||||
)
|
||||
|
||||
Platform email configuration is loaded from:
|
||||
1. Database (admin_settings table) - if settings exist
|
||||
2. Environment variables (.env) - fallback
|
||||
"""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
self.provider = get_provider()
|
||||
# Use configurable provider that checks DB first, then .env
|
||||
self.provider = get_platform_provider(db)
|
||||
# Cache the platform config for use in send_raw
|
||||
self._platform_config = get_platform_email_config(db)
|
||||
self.jinja_env = Environment(loader=BaseLoader())
|
||||
# Cache vendor and feature data to avoid repeated queries
|
||||
self._vendor_cache: dict[int, Any] = {}
|
||||
self._feature_cache: dict[int, set[str]] = {}
|
||||
self._vendor_email_settings_cache: dict[int, Any] = {}
|
||||
self._vendor_tier_cache: dict[int, str | None] = {}
|
||||
|
||||
def _get_vendor(self, vendor_id: int):
|
||||
"""Get vendor with caching."""
|
||||
@@ -419,6 +1017,76 @@ class EmailService:
|
||||
|
||||
return feature_code in self._feature_cache[vendor_id]
|
||||
|
||||
def _get_vendor_email_settings(self, vendor_id: int):
|
||||
"""Get vendor email settings with caching."""
|
||||
if vendor_id not in self._vendor_email_settings_cache:
|
||||
from models.database.vendor_email_settings import VendorEmailSettings
|
||||
|
||||
self._vendor_email_settings_cache[vendor_id] = (
|
||||
self.db.query(VendorEmailSettings)
|
||||
.filter(VendorEmailSettings.vendor_id == vendor_id)
|
||||
.first()
|
||||
)
|
||||
return self._vendor_email_settings_cache[vendor_id]
|
||||
|
||||
def _get_vendor_tier(self, vendor_id: int) -> str | None:
|
||||
"""Get vendor's subscription tier with caching."""
|
||||
if vendor_id not in self._vendor_tier_cache:
|
||||
from app.services.subscription_service import subscription_service
|
||||
|
||||
tier = subscription_service.get_current_tier(self.db, vendor_id)
|
||||
self._vendor_tier_cache[vendor_id] = tier.value if tier else None
|
||||
return self._vendor_tier_cache[vendor_id]
|
||||
|
||||
def _should_add_powered_by_footer(self, vendor_id: int | None) -> bool:
|
||||
"""
|
||||
Check if "Powered by Wizamart" footer should be added.
|
||||
|
||||
Footer is added for Essential and Professional tiers.
|
||||
Business and Enterprise tiers get white-label (no footer).
|
||||
"""
|
||||
if not vendor_id:
|
||||
return False # Platform emails don't get the footer
|
||||
|
||||
tier = self._get_vendor_tier(vendor_id)
|
||||
if not tier:
|
||||
return True # No tier = show footer (shouldn't happen normally)
|
||||
|
||||
return tier.lower() not in WHITELABEL_TIERS
|
||||
|
||||
def _inject_powered_by_footer(
|
||||
self,
|
||||
body_html: str,
|
||||
body_text: str | None,
|
||||
vendor_id: int | None,
|
||||
) -> tuple[str, str | None]:
|
||||
"""
|
||||
Inject "Powered by Wizamart" footer if needed based on tier.
|
||||
|
||||
Returns:
|
||||
Tuple of (modified_html, modified_text)
|
||||
"""
|
||||
if not self._should_add_powered_by_footer(vendor_id):
|
||||
return body_html, body_text
|
||||
|
||||
# Inject footer before closing </body> tag if present, otherwise append
|
||||
if "</body>" in body_html.lower():
|
||||
# Find </body> case-insensitively and inject before it
|
||||
import re
|
||||
body_html = re.sub(
|
||||
r"(</body>)",
|
||||
f"{POWERED_BY_FOOTER_HTML}\\1",
|
||||
body_html,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
else:
|
||||
body_html += POWERED_BY_FOOTER_HTML
|
||||
|
||||
if body_text:
|
||||
body_text += POWERED_BY_FOOTER_TEXT
|
||||
|
||||
return body_html, body_text
|
||||
|
||||
def resolve_language(
|
||||
self,
|
||||
explicit_language: str | None = None,
|
||||
@@ -721,16 +1389,55 @@ class EmailService:
|
||||
related_type: str | None = None,
|
||||
related_id: int | None = None,
|
||||
extra_data: str | None = None,
|
||||
is_platform_email: bool = False,
|
||||
) -> EmailLog:
|
||||
"""
|
||||
Send a raw email without using a template.
|
||||
|
||||
For vendor emails (when vendor_id is provided and is_platform_email=False):
|
||||
- Uses vendor's SMTP/provider settings if configured
|
||||
- Uses vendor's from_email, from_name, reply_to
|
||||
- Adds "Powered by Wizamart" footer for Essential/Professional tiers
|
||||
|
||||
For platform emails (is_platform_email=True or no vendor_id):
|
||||
- Uses platform's email settings from config
|
||||
- No "Powered by Wizamart" footer
|
||||
|
||||
Args:
|
||||
is_platform_email: If True, always use platform settings (for billing, etc.)
|
||||
|
||||
Returns:
|
||||
EmailLog record
|
||||
"""
|
||||
from_email = from_email or settings.email_from_address
|
||||
from_name = from_name or settings.email_from_name
|
||||
reply_to = reply_to or settings.email_reply_to or None
|
||||
# Determine which provider and settings to use
|
||||
vendor_settings = None
|
||||
vendor_provider = None
|
||||
provider_name = self._platform_config.get("provider", settings.email_provider)
|
||||
|
||||
if vendor_id and not is_platform_email:
|
||||
vendor_settings = self._get_vendor_email_settings(vendor_id)
|
||||
if vendor_settings and vendor_settings.is_configured:
|
||||
vendor_provider = get_vendor_provider(vendor_settings)
|
||||
if vendor_provider:
|
||||
# Use vendor's email identity
|
||||
from_email = from_email or vendor_settings.from_email
|
||||
from_name = from_name or vendor_settings.from_name
|
||||
reply_to = reply_to or vendor_settings.reply_to_email
|
||||
provider_name = f"vendor_{vendor_settings.provider}"
|
||||
logger.debug(f"Using vendor email provider: {vendor_settings.provider}")
|
||||
|
||||
# Fall back to platform settings if no vendor provider
|
||||
# Uses DB config if available, otherwise .env
|
||||
if not vendor_provider:
|
||||
from_email = from_email or self._platform_config.get("from_email", settings.email_from_address)
|
||||
from_name = from_name or self._platform_config.get("from_name", settings.email_from_name)
|
||||
reply_to = reply_to or self._platform_config.get("reply_to") or settings.email_reply_to or None
|
||||
|
||||
# Inject "Powered by Wizamart" footer for non-whitelabel tiers
|
||||
if vendor_id and not is_platform_email:
|
||||
body_html, body_text = self._inject_powered_by_footer(
|
||||
body_html, body_text, vendor_id
|
||||
)
|
||||
|
||||
# Create log entry
|
||||
log = EmailLog(
|
||||
@@ -745,7 +1452,7 @@ class EmailService:
|
||||
from_name=from_name,
|
||||
reply_to=reply_to,
|
||||
status=EmailStatus.PENDING.value,
|
||||
provider=settings.email_provider,
|
||||
provider=provider_name,
|
||||
vendor_id=vendor_id,
|
||||
user_id=user_id,
|
||||
related_type=related_type,
|
||||
@@ -755,16 +1462,20 @@ class EmailService:
|
||||
self.db.add(log)
|
||||
self.db.flush()
|
||||
|
||||
# Check if emails are disabled
|
||||
if not settings.email_enabled:
|
||||
# Check if emails are disabled (uses DB config if available)
|
||||
email_enabled = self._platform_config.get("enabled", settings.email_enabled)
|
||||
if not email_enabled:
|
||||
log.status = EmailStatus.FAILED.value
|
||||
log.error_message = "Email sending is disabled"
|
||||
self.db.commit() # noqa: SVC-006 - Email logs are side effects, commit immediately
|
||||
logger.info(f"Email sending disabled, skipping: {to_email}")
|
||||
return log
|
||||
|
||||
# Use vendor provider if available, otherwise platform provider
|
||||
provider_to_use = vendor_provider or self.provider
|
||||
|
||||
# Send email
|
||||
success, message_id, error = self.provider.send(
|
||||
success, message_id, error = provider_to_use.send(
|
||||
to_email=to_email,
|
||||
to_name=to_name,
|
||||
subject=subject,
|
||||
@@ -777,7 +1488,7 @@ class EmailService:
|
||||
|
||||
if success:
|
||||
log.mark_sent(message_id)
|
||||
logger.info(f"Email sent to {to_email}: {subject}")
|
||||
logger.info(f"Email sent to {to_email}: {subject} (via {provider_name})")
|
||||
else:
|
||||
log.mark_failed(error or "Unknown error")
|
||||
logger.error(f"Email failed to {to_email}: {error}")
|
||||
|
||||
444
app/services/vendor_email_settings_service.py
Normal file
444
app/services/vendor_email_settings_service.py
Normal file
@@ -0,0 +1,444 @@
|
||||
# app/services/vendor_email_settings_service.py
|
||||
"""
|
||||
Vendor Email Settings Service.
|
||||
|
||||
Handles CRUD operations for vendor email configuration:
|
||||
- SMTP settings
|
||||
- Advanced providers (SendGrid, Mailgun, SES) - tier-gated
|
||||
- Sender identity (from_email, from_name, reply_to)
|
||||
- Signature/footer customization
|
||||
- Configuration verification via test email
|
||||
"""
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
from datetime import UTC, datetime
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import NotFoundError, ValidationError, AuthorizationError
|
||||
from models.database import (
|
||||
Vendor,
|
||||
VendorEmailSettings,
|
||||
EmailProvider,
|
||||
PREMIUM_EMAIL_PROVIDERS,
|
||||
VendorSubscription,
|
||||
TierCode,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Tiers that allow premium email providers
|
||||
PREMIUM_TIERS = {TierCode.BUSINESS, TierCode.ENTERPRISE}
|
||||
|
||||
|
||||
class VendorEmailSettingsService:
|
||||
"""Service for managing vendor email settings."""
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
# =========================================================================
|
||||
# READ OPERATIONS
|
||||
# =========================================================================
|
||||
|
||||
def get_settings(self, vendor_id: int) -> VendorEmailSettings | None:
|
||||
"""Get email settings for a vendor."""
|
||||
return (
|
||||
self.db.query(VendorEmailSettings)
|
||||
.filter(VendorEmailSettings.vendor_id == vendor_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_settings_or_404(self, vendor_id: int) -> VendorEmailSettings:
|
||||
"""Get email settings or raise 404."""
|
||||
settings = self.get_settings(vendor_id)
|
||||
if not settings:
|
||||
raise NotFoundError(
|
||||
f"Email settings not found for vendor {vendor_id}. "
|
||||
"Configure email settings to send emails."
|
||||
)
|
||||
return settings
|
||||
|
||||
def is_configured(self, vendor_id: int) -> bool:
|
||||
"""Check if vendor has configured email settings."""
|
||||
settings = self.get_settings(vendor_id)
|
||||
return settings is not None and settings.is_configured
|
||||
|
||||
def get_status(self, vendor_id: int) -> dict:
|
||||
"""
|
||||
Get email configuration status for a vendor.
|
||||
|
||||
Returns:
|
||||
dict with is_configured, is_verified, provider, etc.
|
||||
"""
|
||||
settings = self.get_settings(vendor_id)
|
||||
if not settings:
|
||||
return {
|
||||
"is_configured": False,
|
||||
"is_verified": False,
|
||||
"provider": None,
|
||||
"from_email": None,
|
||||
"from_name": None,
|
||||
"message": "Email settings not configured. Configure SMTP to send emails.",
|
||||
}
|
||||
|
||||
return {
|
||||
"is_configured": settings.is_configured,
|
||||
"is_verified": settings.is_verified,
|
||||
"provider": settings.provider,
|
||||
"from_email": settings.from_email,
|
||||
"from_name": settings.from_name,
|
||||
"last_verified_at": settings.last_verified_at.isoformat() if settings.last_verified_at else None,
|
||||
"verification_error": settings.verification_error,
|
||||
"message": self._get_status_message(settings),
|
||||
}
|
||||
|
||||
def _get_status_message(self, settings: VendorEmailSettings) -> str:
|
||||
"""Generate a human-readable status message."""
|
||||
if not settings.is_configured:
|
||||
return "Complete your email configuration to send emails."
|
||||
if not settings.is_verified:
|
||||
return "Email configured but not verified. Send a test email to verify."
|
||||
return "Email settings configured and verified."
|
||||
|
||||
# =========================================================================
|
||||
# WRITE OPERATIONS
|
||||
# =========================================================================
|
||||
|
||||
def create_or_update(
|
||||
self,
|
||||
vendor_id: int,
|
||||
data: dict,
|
||||
current_tier: TierCode | None = None,
|
||||
) -> VendorEmailSettings:
|
||||
"""
|
||||
Create or update vendor email settings.
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID
|
||||
data: Settings data (from_email, from_name, smtp_*, etc.)
|
||||
current_tier: Vendor's current subscription tier (for premium provider validation)
|
||||
|
||||
Returns:
|
||||
Updated VendorEmailSettings
|
||||
"""
|
||||
# Validate premium provider access
|
||||
provider = data.get("provider", "smtp")
|
||||
if provider in [p.value for p in PREMIUM_EMAIL_PROVIDERS]:
|
||||
if current_tier not in PREMIUM_TIERS:
|
||||
raise AuthorizationError(
|
||||
f"Provider '{provider}' requires Business or Enterprise tier. "
|
||||
"Upgrade your plan to use advanced email providers."
|
||||
)
|
||||
|
||||
settings = self.get_settings(vendor_id)
|
||||
if not settings:
|
||||
settings = VendorEmailSettings(vendor_id=vendor_id)
|
||||
self.db.add(settings)
|
||||
|
||||
# Update fields
|
||||
for field in [
|
||||
"from_email",
|
||||
"from_name",
|
||||
"reply_to_email",
|
||||
"signature_text",
|
||||
"signature_html",
|
||||
"provider",
|
||||
# SMTP
|
||||
"smtp_host",
|
||||
"smtp_port",
|
||||
"smtp_username",
|
||||
"smtp_password",
|
||||
"smtp_use_tls",
|
||||
"smtp_use_ssl",
|
||||
# SendGrid
|
||||
"sendgrid_api_key",
|
||||
# Mailgun
|
||||
"mailgun_api_key",
|
||||
"mailgun_domain",
|
||||
# SES
|
||||
"ses_access_key_id",
|
||||
"ses_secret_access_key",
|
||||
"ses_region",
|
||||
]:
|
||||
if field in data and data[field] is not None:
|
||||
# Don't overwrite passwords/keys with empty strings
|
||||
if field.endswith(("_password", "_key", "_access_key")) and data[field] == "":
|
||||
continue
|
||||
setattr(settings, field, data[field])
|
||||
|
||||
# Update configuration status
|
||||
settings.update_configuration_status()
|
||||
|
||||
# Reset verification if provider/credentials changed
|
||||
if any(
|
||||
f in data
|
||||
for f in ["provider", "smtp_host", "smtp_password", "sendgrid_api_key", "mailgun_api_key", "ses_access_key_id"]
|
||||
):
|
||||
settings.is_verified = False
|
||||
settings.verification_error = None
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(settings)
|
||||
|
||||
logger.info(f"Updated email settings for vendor {vendor_id}: provider={settings.provider}")
|
||||
return settings
|
||||
|
||||
def delete(self, vendor_id: int) -> bool:
|
||||
"""Delete email settings for a vendor."""
|
||||
settings = self.get_settings(vendor_id)
|
||||
if settings:
|
||||
self.db.delete(settings)
|
||||
self.db.commit()
|
||||
logger.info(f"Deleted email settings for vendor {vendor_id}")
|
||||
return True
|
||||
return False
|
||||
|
||||
# =========================================================================
|
||||
# VERIFICATION
|
||||
# =========================================================================
|
||||
|
||||
def verify_settings(self, vendor_id: int, test_email: str) -> dict:
|
||||
"""
|
||||
Verify email settings by sending a test email.
|
||||
|
||||
Args:
|
||||
vendor_id: Vendor ID
|
||||
test_email: Email address to send test email to
|
||||
|
||||
Returns:
|
||||
dict with success status and message
|
||||
"""
|
||||
settings = self.get_settings_or_404(vendor_id)
|
||||
|
||||
if not settings.is_fully_configured():
|
||||
raise ValidationError("Email settings incomplete. Configure all required fields first.")
|
||||
|
||||
try:
|
||||
# Send test email based on provider
|
||||
if settings.provider == EmailProvider.SMTP.value:
|
||||
self._send_smtp_test(settings, test_email)
|
||||
elif settings.provider == EmailProvider.SENDGRID.value:
|
||||
self._send_sendgrid_test(settings, test_email)
|
||||
elif settings.provider == EmailProvider.MAILGUN.value:
|
||||
self._send_mailgun_test(settings, test_email)
|
||||
elif settings.provider == EmailProvider.SES.value:
|
||||
self._send_ses_test(settings, test_email)
|
||||
else:
|
||||
raise ValidationError(f"Unknown provider: {settings.provider}")
|
||||
|
||||
# Mark as verified
|
||||
settings.mark_verified()
|
||||
self.db.commit()
|
||||
|
||||
logger.info(f"Email settings verified for vendor {vendor_id}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Test email sent successfully to {test_email}",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
settings.mark_verification_failed(error_msg)
|
||||
self.db.commit()
|
||||
|
||||
logger.warning(f"Email verification failed for vendor {vendor_id}: {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Failed to send test email: {error_msg}",
|
||||
}
|
||||
|
||||
def _send_smtp_test(self, settings: VendorEmailSettings, to_email: str) -> None:
|
||||
"""Send test email via SMTP."""
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = "Wizamart Email Configuration Test"
|
||||
msg["From"] = f"{settings.from_name} <{settings.from_email}>"
|
||||
msg["To"] = to_email
|
||||
|
||||
text_content = (
|
||||
"This is a test email from Wizamart.\n\n"
|
||||
"Your email settings are configured correctly!\n\n"
|
||||
f"Provider: SMTP\n"
|
||||
f"Host: {settings.smtp_host}\n"
|
||||
)
|
||||
html_content = f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2 style="color: #6b46c1;">Email Configuration Test</h2>
|
||||
<p>This is a test email from <strong>Wizamart</strong>.</p>
|
||||
<p style="color: #22c55e; font-weight: bold;">
|
||||
Your email settings are configured correctly!
|
||||
</p>
|
||||
<hr style="border: 1px solid #e5e7eb; margin: 20px 0;">
|
||||
<p style="color: #6b7280; font-size: 12px;">
|
||||
Provider: SMTP<br>
|
||||
Host: {settings.smtp_host}<br>
|
||||
Sent at: {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
msg.attach(MIMEText(text_content, "plain"))
|
||||
msg.attach(MIMEText(html_content, "html"))
|
||||
|
||||
# Connect and send
|
||||
if settings.smtp_use_ssl:
|
||||
server = smtplib.SMTP_SSL(settings.smtp_host, settings.smtp_port)
|
||||
else:
|
||||
server = smtplib.SMTP(settings.smtp_host, settings.smtp_port)
|
||||
if settings.smtp_use_tls:
|
||||
server.starttls()
|
||||
|
||||
server.login(settings.smtp_username, settings.smtp_password)
|
||||
server.sendmail(settings.from_email, to_email, msg.as_string())
|
||||
server.quit()
|
||||
|
||||
def _send_sendgrid_test(self, settings: VendorEmailSettings, to_email: str) -> None:
|
||||
"""Send test email via SendGrid."""
|
||||
try:
|
||||
from sendgrid import SendGridAPIClient
|
||||
from sendgrid.helpers.mail import Mail
|
||||
except ImportError:
|
||||
raise ValidationError("SendGrid library not installed. Contact support.")
|
||||
|
||||
message = Mail(
|
||||
from_email=(settings.from_email, settings.from_name),
|
||||
to_emails=to_email,
|
||||
subject="Wizamart Email Configuration Test",
|
||||
html_content=f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2 style="color: #6b46c1;">Email Configuration Test</h2>
|
||||
<p>This is a test email from <strong>Wizamart</strong>.</p>
|
||||
<p style="color: #22c55e; font-weight: bold;">
|
||||
Your SendGrid settings are configured correctly!
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
)
|
||||
|
||||
sg = SendGridAPIClient(settings.sendgrid_api_key)
|
||||
response = sg.send(message)
|
||||
|
||||
if response.status_code >= 400:
|
||||
raise Exception(f"SendGrid error: {response.status_code}")
|
||||
|
||||
def _send_mailgun_test(self, settings: VendorEmailSettings, to_email: str) -> None:
|
||||
"""Send test email via Mailgun."""
|
||||
import requests
|
||||
|
||||
response = requests.post(
|
||||
f"https://api.mailgun.net/v3/{settings.mailgun_domain}/messages",
|
||||
auth=("api", settings.mailgun_api_key),
|
||||
data={
|
||||
"from": f"{settings.from_name} <{settings.from_email}>",
|
||||
"to": to_email,
|
||||
"subject": "Wizamart Email Configuration Test",
|
||||
"html": f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2 style="color: #6b46c1;">Email Configuration Test</h2>
|
||||
<p>This is a test email from <strong>Wizamart</strong>.</p>
|
||||
<p style="color: #22c55e; font-weight: bold;">
|
||||
Your Mailgun settings are configured correctly!
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
if response.status_code >= 400:
|
||||
raise Exception(f"Mailgun error: {response.text}")
|
||||
|
||||
def _send_ses_test(self, settings: VendorEmailSettings, to_email: str) -> None:
|
||||
"""Send test email via Amazon SES."""
|
||||
try:
|
||||
import boto3
|
||||
except ImportError:
|
||||
raise ValidationError("boto3 library not installed. Contact support.")
|
||||
|
||||
client = boto3.client(
|
||||
"ses",
|
||||
region_name=settings.ses_region,
|
||||
aws_access_key_id=settings.ses_access_key_id,
|
||||
aws_secret_access_key=settings.ses_secret_access_key,
|
||||
)
|
||||
|
||||
client.send_email(
|
||||
Source=f"{settings.from_name} <{settings.from_email}>",
|
||||
Destination={"ToAddresses": [to_email]},
|
||||
Message={
|
||||
"Subject": {"Data": "Wizamart Email Configuration Test"},
|
||||
"Body": {
|
||||
"Html": {
|
||||
"Data": f"""
|
||||
<html>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2 style="color: #6b46c1;">Email Configuration Test</h2>
|
||||
<p>This is a test email from <strong>Wizamart</strong>.</p>
|
||||
<p style="color: #22c55e; font-weight: bold;">
|
||||
Your Amazon SES settings are configured correctly!
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# TIER HELPERS
|
||||
# =========================================================================
|
||||
|
||||
def get_available_providers(self, tier: TierCode | None) -> list[dict]:
|
||||
"""
|
||||
Get list of available email providers for a tier.
|
||||
|
||||
Returns list of providers with availability status.
|
||||
"""
|
||||
providers = [
|
||||
{
|
||||
"code": EmailProvider.SMTP.value,
|
||||
"name": "SMTP",
|
||||
"description": "Standard SMTP email server",
|
||||
"available": True,
|
||||
"tier_required": None,
|
||||
},
|
||||
{
|
||||
"code": EmailProvider.SENDGRID.value,
|
||||
"name": "SendGrid",
|
||||
"description": "SendGrid email delivery platform",
|
||||
"available": tier in PREMIUM_TIERS if tier else False,
|
||||
"tier_required": "business",
|
||||
},
|
||||
{
|
||||
"code": EmailProvider.MAILGUN.value,
|
||||
"name": "Mailgun",
|
||||
"description": "Mailgun email API",
|
||||
"available": tier in PREMIUM_TIERS if tier else False,
|
||||
"tier_required": "business",
|
||||
},
|
||||
{
|
||||
"code": EmailProvider.SES.value,
|
||||
"name": "Amazon SES",
|
||||
"description": "Amazon Simple Email Service",
|
||||
"available": tier in PREMIUM_TIERS if tier else False,
|
||||
"tier_required": "business",
|
||||
},
|
||||
]
|
||||
return providers
|
||||
|
||||
|
||||
# Module-level service factory
|
||||
def get_vendor_email_settings_service(db: Session) -> VendorEmailSettingsService:
|
||||
"""Factory function to get a VendorEmailSettingsService instance."""
|
||||
return VendorEmailSettingsService(db)
|
||||
Reference in New Issue
Block a user