# app/services/email_service.py
"""
Email service with multi-provider support.
Supports:
- SMTP (default)
- SendGrid
- Mailgun
- Amazon SES
Features:
- Multi-language templates from database
- Vendor template overrides
- Jinja2 template rendering
- Email logging and tracking
- Queue support via background tasks
- Branding based on vendor tier (whitelabel)
Language Resolution (priority order):
1. Explicit language parameter
2. Customer's preferred language (if customer context)
3. Vendor's storefront language
4. Platform default (en)
Template Resolution (priority order):
1. Vendor override (if vendor_id and template is not platform-only)
2. Platform template
3. English fallback (if requested language not found)
"""
import json
import logging
import smtplib
from abc import ABC, abstractmethod
from dataclasses import dataclass
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import Any
from jinja2 import Environment, BaseLoader
from sqlalchemy.orm import Session
from app.core.config import settings
from models.database.email import EmailLog, EmailStatus, EmailTemplate
from models.database.vendor_email_template import VendorEmailTemplate
logger = logging.getLogger(__name__)
# Platform branding constants
PLATFORM_NAME = "Wizamart"
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 = """
"""
POWERED_BY_FOOTER_TEXT = "\n\n---\nPowered by Wizamart - https://wizamart.com"
@dataclass
class ResolvedTemplate:
"""Resolved template content after checking vendor overrides."""
subject: str
body_html: str
body_text: str | None
is_vendor_override: bool
template_id: int | None # Platform template ID (None if vendor override)
template_code: str
language: str
@dataclass
class BrandingContext:
"""Branding variables for email templates."""
platform_name: str
platform_logo_url: str | None
support_email: str
vendor_name: str | None
vendor_logo_url: str | None
is_whitelabel: bool
# =============================================================================
# EMAIL PROVIDER ABSTRACTION
# =============================================================================
class EmailProvider(ABC):
"""Abstract base class for email providers."""
@abstractmethod
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]:
"""
Send an email.
Returns:
tuple: (success, provider_message_id, error_message)
"""
pass
class SMTPProvider(EmailProvider):
"""SMTP email provider."""
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
# Attach text and HTML parts
if body_text:
msg.attach(MIMEText(body_text, "plain", "utf-8"))
msg.attach(MIMEText(body_html, "html", "utf-8"))
# Connect and send (10-second timeout to fail fast)
timeout = 10
if settings.smtp_use_ssl:
server = smtplib.SMTP_SSL(settings.smtp_host, settings.smtp_port, timeout=timeout)
else:
server = smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=timeout)
try:
if settings.smtp_use_tls and not settings.smtp_use_ssl:
server.starttls()
if settings.smtp_user and settings.smtp_password:
server.login(settings.smtp_user, 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"SMTP send error: {e}")
return False, None, str(e)
class SendGridProvider(EmailProvider):
"""SendGrid email provider."""
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(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. Run: pip install sendgrid"
except Exception as e:
logger.error(f"SendGrid send error: {e}")
return False, None, str(e)
class MailgunProvider(EmailProvider):
"""Mailgun email provider."""
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/{settings.mailgun_domain}/messages",
auth=("api", 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"Mailgun send error: {e}")
return False, None, str(e)
class SESProvider(EmailProvider):
"""Amazon SES email provider."""
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=settings.aws_region,
aws_access_key_id=settings.aws_access_key_id,
aws_secret_access_key=settings.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. Run: pip install boto3"
except Exception as e:
logger.error(f"SES send error: {e}")
return False, None, str(e)
class DebugProvider(EmailProvider):
"""Debug provider - logs emails instead of sending."""
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]:
logger.info(
f"\n{'='*60}\n"
f"DEBUG EMAIL\n"
f"{'='*60}\n"
f"To: {to_name} <{to_email}>\n"
f"From: {from_name} <{from_email}>\n"
f"Reply-To: {reply_to}\n"
f"Subject: {subject}\n"
f"{'='*60}\n"
f"Body (text):\n{body_text or '(none)'}\n"
f"{'='*60}\n"
f"Body (html):\n{body_html[:500]}...\n"
f"{'='*60}\n"
)
return True, f"debug-{to_email}", None
# =============================================================================
# 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"))
# Use 10-second timeout to fail fast on bad SMTP settings
timeout = 10
if self.config.get("smtp_use_ssl"):
server = smtplib.SMTP_SSL(self.config["smtp_host"], self.config["smtp_port"], timeout=timeout)
else:
server = smtplib.SMTP(self.config["smtp_host"], self.config["smtp_port"], timeout=timeout)
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 (10-second timeout to fail fast)
timeout = 10
if self.settings.smtp_use_ssl:
server = smtplib.SMTP_SSL(self.settings.smtp_host, self.settings.smtp_port, timeout=timeout)
else:
server = smtplib.SMTP(self.settings.smtp_host, self.settings.smtp_port, timeout=timeout)
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
# =============================================================================
def get_provider() -> EmailProvider:
"""Get the configured email provider."""
if settings.email_debug:
return DebugProvider()
provider_map = {
"smtp": SMTPProvider,
"sendgrid": SendGridProvider,
"mailgun": MailgunProvider,
"ses": SESProvider,
}
provider_class = provider_map.get(settings.email_provider.lower())
if not provider_class:
logger.warning(f"Unknown email provider: {settings.email_provider}, using SMTP")
return SMTPProvider()
return provider_class()
class EmailService:
"""
Email service for sending templated emails.
Usage:
email_service = EmailService(db)
# Send using database template with vendor override support
email_service.send_template(
template_code="signup_welcome",
to_email="user@example.com",
to_name="John Doe",
variables={"first_name": "John", "login_url": "https://..."},
vendor_id=1,
# Language is resolved automatically from vendor/customer settings
)
# Send raw email
email_service.send_raw(
to_email="user@example.com",
subject="Hello",
body_html="Hello
",
)
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
# 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."""
if vendor_id not in self._vendor_cache:
from models.database.vendor import Vendor
self._vendor_cache[vendor_id] = (
self.db.query(Vendor).filter(Vendor.id == vendor_id).first()
)
return self._vendor_cache[vendor_id]
def _has_feature(self, vendor_id: int, feature_code: str) -> bool:
"""Check if vendor has a specific feature enabled."""
if vendor_id not in self._feature_cache:
from app.services.feature_service import feature_service
try:
features = feature_service.get_vendor_features(self.db, vendor_id)
# Convert to set of feature codes
self._feature_cache[vendor_id] = {f.code for f in features.features}
except Exception:
self._feature_cache[vendor_id] = set()
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