feat: Add email settings management and templates functionality
- Implemented email settings configuration in the admin panel, allowing for SMTP settings and notification preferences. - Created a new template for email settings with fields for SMTP host, port, username, password, sender address, and recipients. - Added JavaScript functionality to handle loading, saving, and validating email settings. - Introduced email templates management, enabling the listing, editing, and saving of email templates. - Updated navigation to include links to email settings and templates. - Added tests for email settings and templates to ensure proper functionality and validation.
This commit is contained in:
@@ -6,6 +6,17 @@ import logging
|
||||
|
||||
from .. import auth, settings
|
||||
from ..database import delete_app_setting, get_app_settings, get_subscribers, update_app_setting
|
||||
from ..services.email_settings import (
|
||||
EMAIL_SETTINGS_DEFINITIONS,
|
||||
load_email_settings,
|
||||
persist_email_settings,
|
||||
validate_email_settings,
|
||||
)
|
||||
from ..services.email_templates import (
|
||||
list_templates as list_email_templates,
|
||||
load_template as load_email_template,
|
||||
persist_template as persist_email_template,
|
||||
)
|
||||
|
||||
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
||||
|
||||
@@ -96,6 +107,13 @@ def email_templates():
|
||||
return render_template('admin/admin_email_templates.html')
|
||||
|
||||
|
||||
@bp.route('/email-settings')
|
||||
@auth.login_required
|
||||
def email_settings_page():
|
||||
"""Render the dedicated email settings management page."""
|
||||
return render_template('admin/admin_email_settings.html')
|
||||
|
||||
|
||||
@bp.route("/api/settings", methods=["GET"])
|
||||
@auth.login_required
|
||||
def get_settings_api():
|
||||
@@ -179,6 +197,121 @@ def delete_setting_api(key: str):
|
||||
return jsonify({"status": "error", "message": "Failed to delete setting."}), 500
|
||||
|
||||
|
||||
@bp.route("/api/email-settings", methods=["GET"])
|
||||
@auth.login_required
|
||||
def get_email_settings_api():
|
||||
"""Return the current email settings with field metadata."""
|
||||
try:
|
||||
data = load_email_settings()
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
"settings": data,
|
||||
"schema": EMAIL_SETTINGS_DEFINITIONS,
|
||||
})
|
||||
except Exception as exc: # pragma: no cover - logged and surfaced to client
|
||||
logging.exception("Failed to load email settings: %s", exc)
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Failed to load email settings."
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route("/api/email-settings", methods=["PUT"])
|
||||
@auth.login_required
|
||||
def update_email_settings_api():
|
||||
"""Validate and persist email settings updates."""
|
||||
try:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
errors = validate_email_settings(payload)
|
||||
if errors:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Email settings validation failed.",
|
||||
"errors": errors,
|
||||
}), 400
|
||||
|
||||
updated = persist_email_settings(payload)
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
"message": "Email settings updated successfully.",
|
||||
"settings": updated,
|
||||
})
|
||||
except Exception as exc: # pragma: no cover - logged and surfaced to client
|
||||
logging.exception("Failed to update email settings: %s", exc)
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Failed to update email settings."
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route("/api/email-templates", methods=["GET"])
|
||||
@auth.login_required
|
||||
def list_email_templates_api():
|
||||
"""Return metadata for editable email templates."""
|
||||
try:
|
||||
templates = list_email_templates()
|
||||
return jsonify({"status": "ok", "templates": templates})
|
||||
except Exception as exc: # pragma: no cover
|
||||
logging.exception("Failed to list email templates: %s", exc)
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Failed to load email templates."
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route("/api/email-templates/<template_id>", methods=["GET"])
|
||||
@auth.login_required
|
||||
def get_email_template_api(template_id: str):
|
||||
"""Return the requested email template."""
|
||||
try:
|
||||
template = load_email_template(template_id)
|
||||
return jsonify({"status": "ok", "template": template})
|
||||
except KeyError:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Email template not found."
|
||||
}), 404
|
||||
except Exception as exc: # pragma: no cover
|
||||
logging.exception(
|
||||
"Failed to load email template %s: %s", template_id, exc)
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Failed to load email template."
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route("/api/email-templates/<template_id>", methods=["PUT"])
|
||||
@auth.login_required
|
||||
def update_email_template_api(template_id: str):
|
||||
"""Persist updates to an email template."""
|
||||
try:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
content = payload.get("content", "")
|
||||
if not isinstance(content, str) or not content.strip():
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Template content is required.",
|
||||
}), 400
|
||||
|
||||
updated = persist_email_template(template_id, content)
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
"template": updated,
|
||||
})
|
||||
except KeyError:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Email template not found."
|
||||
}), 404
|
||||
except Exception as exc: # pragma: no cover
|
||||
logging.exception(
|
||||
"Failed to update email template %s: %s", template_id, exc)
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Failed to update email template."
|
||||
}), 500
|
||||
|
||||
|
||||
@bp.route("/api/newsletter", methods=["GET"])
|
||||
@auth.login_required
|
||||
def get_subscribers_api():
|
||||
|
||||
@@ -6,12 +6,12 @@ import smtplib
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from email.message import EmailMessage
|
||||
from typing import Any, Dict, Tuple
|
||||
from typing import Any, Dict, Tuple, cast
|
||||
|
||||
from .. import settings
|
||||
from ..database import save_contact
|
||||
from ..metrics import record_submission
|
||||
from ..utils import is_valid_email
|
||||
from .email_settings import load_effective_smtp_settings
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -70,12 +70,22 @@ def persist_submission(submission: ContactSubmission) -> int:
|
||||
|
||||
def send_notification(submission: ContactSubmission) -> bool:
|
||||
"""Send an email notification for the submission if SMTP is configured."""
|
||||
if not settings.SMTP_SETTINGS["host"] or not settings.SMTP_SETTINGS["recipients"]:
|
||||
smtp_config = load_effective_smtp_settings()
|
||||
|
||||
if not smtp_config.get("notify_contact_form"):
|
||||
logging.info("Contact notifications disabled; skipping email dispatch")
|
||||
return False
|
||||
|
||||
if not smtp_config.get("host") or not smtp_config.get("recipients"):
|
||||
logging.info("SMTP not configured; skipping email notification")
|
||||
return False
|
||||
|
||||
sender = settings.SMTP_SETTINGS["sender"] or "no-reply@example.com"
|
||||
recipients = settings.SMTP_SETTINGS["recipients"]
|
||||
sender = smtp_config.get("sender") or "no-reply@example.com"
|
||||
recipients = smtp_config.get("recipients", [])
|
||||
host = cast(str, smtp_config.get("host"))
|
||||
port = int(smtp_config.get("port") or 0)
|
||||
username = smtp_config.get("username") or None
|
||||
password = smtp_config.get("password") or ""
|
||||
|
||||
msg = EmailMessage()
|
||||
msg["Subject"] = f"Neue Kontaktanfrage von {submission.name}"
|
||||
@@ -98,12 +108,11 @@ def send_notification(submission: ContactSubmission) -> bool:
|
||||
)
|
||||
|
||||
try:
|
||||
with smtplib.SMTP(settings.SMTP_SETTINGS["host"], settings.SMTP_SETTINGS["port"], timeout=15) as server:
|
||||
if settings.SMTP_SETTINGS["use_tls"]:
|
||||
with smtplib.SMTP(host, port, timeout=15) as server:
|
||||
if smtp_config.get("use_tls"):
|
||||
server.starttls()
|
||||
if settings.SMTP_SETTINGS["username"]:
|
||||
server.login(
|
||||
settings.SMTP_SETTINGS["username"], settings.SMTP_SETTINGS["password"] or "")
|
||||
if username:
|
||||
server.login(username, password)
|
||||
server.send_message(msg)
|
||||
logging.info("Notification email dispatched to %s", recipients)
|
||||
return True
|
||||
|
||||
224
server/services/email_settings.py
Normal file
224
server/services/email_settings.py
Normal file
@@ -0,0 +1,224 @@
|
||||
"""Email settings service helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from ..database import get_app_settings, update_app_setting
|
||||
from ..settings import (
|
||||
SMTP_SETTINGS,
|
||||
EMAIL_NOTIFY_CONTACT_FORM,
|
||||
EMAIL_NOTIFY_NEWSLETTER_SIGNUPS,
|
||||
)
|
||||
from ..utils import normalize_recipients, is_valid_email
|
||||
|
||||
EMAIL_SETTING_PREFIX = "email_"
|
||||
|
||||
# Definition metadata describing each configurable field.
|
||||
EMAIL_SETTINGS_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
||||
"smtp_host": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"default": SMTP_SETTINGS.get("host") or "",
|
||||
"description": "Hostname or IP address of the SMTP server.",
|
||||
},
|
||||
"smtp_port": {
|
||||
"type": "int",
|
||||
"required": True,
|
||||
"default": int(SMTP_SETTINGS.get("port") or 587),
|
||||
"description": "Port number used to connect to the SMTP server.",
|
||||
},
|
||||
"smtp_username": {
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": SMTP_SETTINGS.get("username") or "",
|
||||
"description": "Username for authenticating with the SMTP server (optional).",
|
||||
},
|
||||
"smtp_password": {
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"default": SMTP_SETTINGS.get("password") or "",
|
||||
"description": "Password for authenticating with the SMTP server (optional).",
|
||||
},
|
||||
"smtp_sender": {
|
||||
"type": "string",
|
||||
"required": True,
|
||||
"default": SMTP_SETTINGS.get("sender") or SMTP_SETTINGS.get("username") or "",
|
||||
"description": "Default email address used in the From header when sending mail.",
|
||||
},
|
||||
"smtp_use_tls": {
|
||||
"type": "bool",
|
||||
"required": True,
|
||||
"default": bool(SMTP_SETTINGS.get("use_tls", True)),
|
||||
"description": "Enable TLS when communicating with the SMTP server.",
|
||||
},
|
||||
"smtp_recipients": {
|
||||
"type": "list",
|
||||
"required": False,
|
||||
"default": SMTP_SETTINGS.get("recipients") or [],
|
||||
"description": "Comma separated list of notification recipient emails.",
|
||||
},
|
||||
"notify_contact_form": {
|
||||
"type": "bool",
|
||||
"required": True,
|
||||
"default": EMAIL_NOTIFY_CONTACT_FORM,
|
||||
"description": "Send notification emails for new contact form submissions.",
|
||||
},
|
||||
"notify_newsletter_signups": {
|
||||
"type": "bool",
|
||||
"required": True,
|
||||
"default": EMAIL_NOTIFY_NEWSLETTER_SIGNUPS,
|
||||
"description": "Send notification emails for new newsletter subscriber signups.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _storage_key(field: str) -> str:
|
||||
"""Return the persistent storage key for a given field."""
|
||||
return f"{EMAIL_SETTING_PREFIX}{field}"
|
||||
|
||||
|
||||
def _ensure_bool(value: Any) -> bool:
|
||||
"""Coerce user-supplied values into boolean form."""
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
if value is None:
|
||||
return False
|
||||
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _ensure_int(value: Any) -> Tuple[bool, int | None]:
|
||||
"""Try to coerce a value into an integer."""
|
||||
try:
|
||||
return True, int(value)
|
||||
except (TypeError, ValueError):
|
||||
return False, None
|
||||
|
||||
|
||||
def _serialize(field: str, value: Any) -> str:
|
||||
"""Serialize a typed value to its string representation for storage."""
|
||||
definition = EMAIL_SETTINGS_DEFINITIONS[field]
|
||||
if definition["type"] == "bool":
|
||||
return "true" if bool(value) else "false"
|
||||
if definition["type"] == "int":
|
||||
return str(int(value))
|
||||
if definition["type"] == "list":
|
||||
if isinstance(value, str):
|
||||
value = normalize_recipients(value)
|
||||
return ", ".join(value)
|
||||
return str(value or "")
|
||||
|
||||
|
||||
def _deserialize(field: str, raw_value: Any) -> Any:
|
||||
"""Deserialize the stored string representation for a field."""
|
||||
definition = EMAIL_SETTINGS_DEFINITIONS[field]
|
||||
if raw_value is None or raw_value == "":
|
||||
return definition["default"]
|
||||
|
||||
if definition["type"] == "bool":
|
||||
return _ensure_bool(raw_value)
|
||||
if definition["type"] == "int":
|
||||
ok, coerced = _ensure_int(raw_value)
|
||||
return coerced if ok and coerced is not None else definition["default"]
|
||||
if definition["type"] == "list":
|
||||
if isinstance(raw_value, (list, tuple)):
|
||||
return [item for item in raw_value if item]
|
||||
return normalize_recipients(str(raw_value))
|
||||
return str(raw_value)
|
||||
|
||||
|
||||
def load_email_settings() -> Dict[str, Any]:
|
||||
"""Load email settings, combining stored overrides with defaults."""
|
||||
stored = get_app_settings()
|
||||
results: Dict[str, Any] = {}
|
||||
for field in EMAIL_SETTINGS_DEFINITIONS:
|
||||
raw_value = stored.get(_storage_key(field))
|
||||
results[field] = _deserialize(field, raw_value)
|
||||
return results
|
||||
|
||||
|
||||
def load_effective_smtp_settings() -> Dict[str, Any]:
|
||||
"""Return SMTP configuration merging persisted settings with environment defaults."""
|
||||
effective = load_email_settings()
|
||||
recipients_value = effective.get("smtp_recipients")
|
||||
if isinstance(recipients_value, list):
|
||||
effective_recipients = [item for item in recipients_value if item]
|
||||
else:
|
||||
effective_recipients = normalize_recipients(str(recipients_value or ""))
|
||||
|
||||
return {
|
||||
"host": effective.get("smtp_host") or SMTP_SETTINGS.get("host"),
|
||||
"port": effective.get("smtp_port") or SMTP_SETTINGS.get("port"),
|
||||
"username": effective.get("smtp_username") or SMTP_SETTINGS.get("username"),
|
||||
"password": effective.get("smtp_password") or SMTP_SETTINGS.get("password"),
|
||||
"sender": effective.get("smtp_sender") or SMTP_SETTINGS.get("sender") or SMTP_SETTINGS.get("username"),
|
||||
"use_tls": bool(effective.get("smtp_use_tls")) if effective.get("smtp_use_tls") is not None else SMTP_SETTINGS.get("use_tls", True),
|
||||
"recipients": effective_recipients,
|
||||
"notify_contact_form": bool(effective.get("notify_contact_form")),
|
||||
"notify_newsletter_signups": bool(effective.get("notify_newsletter_signups")),
|
||||
}
|
||||
|
||||
|
||||
def validate_email_settings(payload: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Validate user supplied email settings. Returns mapping of field -> error."""
|
||||
errors: Dict[str, str] = {}
|
||||
|
||||
smtp_host = str(payload.get("smtp_host", "")).strip()
|
||||
if not smtp_host:
|
||||
errors["smtp_host"] = "SMTP host is required."
|
||||
|
||||
ok, port_value = _ensure_int(payload.get("smtp_port"))
|
||||
if not ok or port_value is None or not (1 <= port_value <= 65535):
|
||||
errors["smtp_port"] = "SMTP port must be a valid integer between 1 and 65535."
|
||||
|
||||
sender = str(payload.get("smtp_sender", "")).strip()
|
||||
if not sender:
|
||||
errors["smtp_sender"] = "Sender email is required."
|
||||
elif not is_valid_email(sender):
|
||||
errors["smtp_sender"] = "Sender must be a valid email address."
|
||||
|
||||
recipients_raw = payload.get("smtp_recipients")
|
||||
if isinstance(recipients_raw, list):
|
||||
recipient_list = [item.strip() for item in recipients_raw if item]
|
||||
else:
|
||||
recipient_list = normalize_recipients(str(recipients_raw or ""))
|
||||
|
||||
if recipient_list:
|
||||
invalid = [addr for addr in recipient_list if not is_valid_email(addr)]
|
||||
if invalid:
|
||||
errors["smtp_recipients"] = "Invalid recipient email(s): " + ", ".join(
|
||||
invalid)
|
||||
|
||||
for flag_field in ("smtp_use_tls", "notify_contact_form", "notify_newsletter_signups"):
|
||||
value = payload.get(flag_field)
|
||||
if value is None:
|
||||
continue
|
||||
# Ensure value is interpretable as boolean.
|
||||
if not isinstance(value, bool) and str(value).strip().lower() not in {"1", "0", "true", "false", "yes", "no", "on", "off"}:
|
||||
errors[flag_field] = "Provide a boolean value."
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def persist_email_settings(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Persist validated email settings and return the normalized payload."""
|
||||
normalized: Dict[str, Any] = {}
|
||||
for field, meta in EMAIL_SETTINGS_DEFINITIONS.items():
|
||||
value = payload.get(field, meta["default"])
|
||||
if meta["type"] == "bool":
|
||||
value = _ensure_bool(value)
|
||||
elif meta["type"] == "int":
|
||||
ok, coerced = _ensure_int(value)
|
||||
value = coerced if ok and coerced is not None else meta["default"]
|
||||
elif meta["type"] == "list":
|
||||
if isinstance(value, list):
|
||||
value = [item.strip() for item in value if item]
|
||||
else:
|
||||
value = normalize_recipients(str(value or ""))
|
||||
else:
|
||||
value = str(value or "").strip()
|
||||
|
||||
# Persist using string representation
|
||||
update_app_setting(_storage_key(field), _serialize(field, value))
|
||||
normalized[field] = value
|
||||
|
||||
return normalized
|
||||
86
server/services/email_templates.py
Normal file
86
server/services/email_templates.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Email template management helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List
|
||||
|
||||
from ..database import get_app_settings, update_app_setting
|
||||
from .. import settings
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EmailTemplateDefinition:
|
||||
"""Definition metadata describing an editable email template."""
|
||||
|
||||
id: str
|
||||
setting_key: str
|
||||
name: str
|
||||
description: str
|
||||
default_content: str
|
||||
|
||||
|
||||
EMAIL_TEMPLATE_DEFINITIONS: Dict[str, EmailTemplateDefinition] = {
|
||||
"newsletter_confirmation": EmailTemplateDefinition(
|
||||
id="newsletter_confirmation",
|
||||
setting_key="newsletter_confirmation_template",
|
||||
name="Newsletter Confirmation",
|
||||
description="HTML email sent to subscribers immediately after they confirm their newsletter signup.",
|
||||
default_content=getattr(
|
||||
settings, "NEWSLETTER_CONFIRMATION_TEMPLATE", "").strip(),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def list_templates() -> List[dict]:
|
||||
"""Return a list of metadata describing available email templates."""
|
||||
return [
|
||||
{
|
||||
"id": definition.id,
|
||||
"name": definition.name,
|
||||
"description": definition.description,
|
||||
}
|
||||
for definition in EMAIL_TEMPLATE_DEFINITIONS.values()
|
||||
]
|
||||
|
||||
|
||||
def _load_stored_templates() -> dict:
|
||||
"""Return stored template values keyed by their setting key."""
|
||||
return get_app_settings()
|
||||
|
||||
|
||||
def load_template(template_id: str) -> dict:
|
||||
"""Return template metadata and content for the requested template."""
|
||||
if template_id not in EMAIL_TEMPLATE_DEFINITIONS:
|
||||
raise KeyError(template_id)
|
||||
|
||||
definition = EMAIL_TEMPLATE_DEFINITIONS[template_id]
|
||||
stored = _load_stored_templates()
|
||||
content = stored.get(definition.setting_key, definition.default_content)
|
||||
|
||||
return {
|
||||
"id": definition.id,
|
||||
"name": definition.name,
|
||||
"description": definition.description,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
|
||||
def persist_template(template_id: str, content: str) -> dict:
|
||||
"""Persist template content and return the updated template payload."""
|
||||
if template_id not in EMAIL_TEMPLATE_DEFINITIONS:
|
||||
raise KeyError(template_id)
|
||||
|
||||
definition = EMAIL_TEMPLATE_DEFINITIONS[template_id]
|
||||
content = (content or "").strip()
|
||||
update_app_setting(definition.setting_key, content)
|
||||
return load_template(template_id)
|
||||
|
||||
|
||||
def get_template_content(template_id: str) -> str:
|
||||
"""Return just the template body, falling back to defaults when unset."""
|
||||
if template_id not in EMAIL_TEMPLATE_DEFINITIONS:
|
||||
raise KeyError(template_id)
|
||||
|
||||
definition = EMAIL_TEMPLATE_DEFINITIONS[template_id]
|
||||
stored = _load_stored_templates()
|
||||
return stored.get(definition.setting_key, definition.default_content)
|
||||
@@ -2,9 +2,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import cast
|
||||
|
||||
from ..database import save_subscriber, delete_subscriber, update_subscriber
|
||||
from ..utils import is_valid_email
|
||||
from .email_settings import load_effective_smtp_settings
|
||||
from .email_templates import get_template_content
|
||||
|
||||
|
||||
def validate_email(email: str) -> bool:
|
||||
@@ -31,30 +34,31 @@ def update_email(old_email: str, new_email: str) -> bool:
|
||||
def send_subscription_confirmation(email: str) -> bool:
|
||||
"""Send a confirmation email to the subscriber."""
|
||||
import logging
|
||||
from .. import settings
|
||||
|
||||
if not settings.SMTP_SETTINGS["host"]:
|
||||
smtp_config = load_effective_smtp_settings()
|
||||
if not smtp_config.get("notify_newsletter_signups"):
|
||||
logging.info(
|
||||
"Newsletter signup notifications disabled; skipping email")
|
||||
return False
|
||||
|
||||
host = smtp_config.get("host")
|
||||
port = int(smtp_config.get("port") or 0)
|
||||
username = smtp_config.get("username")
|
||||
password = smtp_config.get("password") or ""
|
||||
|
||||
if not host:
|
||||
logging.info("SMTP not configured; skipping confirmation email")
|
||||
return False
|
||||
|
||||
# Get template from settings or default
|
||||
template = getattr(settings, 'NEWSLETTER_CONFIRMATION_TEMPLATE', """
|
||||
<html>
|
||||
<body>
|
||||
<h2>Welcome to our Newsletter!</h2>
|
||||
<p>Thank you for subscribing to our newsletter. You're now part of our community and will receive updates on our latest news and offers.</p>
|
||||
<p>If you wish to unsubscribe at any time, you can do so by visiting our <a href="https://your-domain.com/newsletter/manage">subscription management page</a>.</p>
|
||||
<p>Best regards,<br>The Team</p>
|
||||
</body>
|
||||
</html>
|
||||
""").strip()
|
||||
# Get template content (persisted or default)
|
||||
template = get_template_content("newsletter_confirmation")
|
||||
|
||||
try:
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
sender = settings.SMTP_SETTINGS["sender"] or "noreply@example.com"
|
||||
sender = smtp_config.get("sender") or "noreply@example.com"
|
||||
|
||||
# Create message
|
||||
msg = MIMEMultipart('alternative')
|
||||
@@ -67,28 +71,33 @@ def send_subscription_confirmation(email: str) -> bool:
|
||||
msg.attach(html_part)
|
||||
|
||||
# Send email
|
||||
with smtplib.SMTP(settings.SMTP_SETTINGS["host"], settings.SMTP_SETTINGS["port"], timeout=15) as server:
|
||||
if settings.SMTP_SETTINGS["use_tls"]:
|
||||
with smtplib.SMTP(cast(str, host), port, timeout=15) as server:
|
||||
if smtp_config.get("use_tls"):
|
||||
server.starttls()
|
||||
if settings.SMTP_SETTINGS["username"]:
|
||||
server.login(
|
||||
settings.SMTP_SETTINGS["username"], settings.SMTP_SETTINGS["password"] or "")
|
||||
if username:
|
||||
server.login(username, password)
|
||||
server.send_message(msg)
|
||||
|
||||
logging.info("Confirmation email sent to %s", email)
|
||||
return True
|
||||
|
||||
except Exception as exc:
|
||||
logging.exception("Failed to send confirmation email to %s: %s", email, exc)
|
||||
logging.exception(
|
||||
"Failed to send confirmation email to %s: %s", email, exc)
|
||||
return False
|
||||
|
||||
|
||||
def send_newsletter_to_subscribers(subject: str, content: str, emails: list[str], sender_name: str | None = None) -> int:
|
||||
"""Send newsletter to list of email addresses. Returns count of successful sends."""
|
||||
import logging
|
||||
from .. import settings
|
||||
smtp_config = load_effective_smtp_settings()
|
||||
|
||||
if not settings.SMTP_SETTINGS["host"]:
|
||||
host = smtp_config.get("host")
|
||||
port = int(smtp_config.get("port") or 0)
|
||||
username = smtp_config.get("username")
|
||||
password = smtp_config.get("password")
|
||||
|
||||
if not host:
|
||||
logging.error("SMTP not configured, cannot send newsletter")
|
||||
return 0
|
||||
|
||||
@@ -100,7 +109,7 @@ def send_newsletter_to_subscribers(subject: str, content: str, emails: list[str]
|
||||
# Create message
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = subject
|
||||
msg['From'] = settings.SMTP_SETTINGS["sender"] or "noreply@example.com"
|
||||
msg['From'] = smtp_config.get("sender") or "noreply@example.com"
|
||||
|
||||
# Format content
|
||||
formatted_content = content.replace('\n', '<br>')
|
||||
@@ -118,13 +127,12 @@ def send_newsletter_to_subscribers(subject: str, content: str, emails: list[str]
|
||||
|
||||
# Send to each recipient individually for better deliverability
|
||||
success_count = 0
|
||||
with smtplib.SMTP(settings.SMTP_SETTINGS["host"], settings.SMTP_SETTINGS["port"]) as server:
|
||||
if settings.SMTP_SETTINGS["use_tls"]:
|
||||
with smtplib.SMTP(cast(str, host), port) as server:
|
||||
if smtp_config.get("use_tls"):
|
||||
server.starttls()
|
||||
|
||||
if settings.SMTP_SETTINGS["username"] and settings.SMTP_SETTINGS["password"]:
|
||||
server.login(
|
||||
settings.SMTP_SETTINGS["username"], settings.SMTP_SETTINGS["password"])
|
||||
if username and password is not None:
|
||||
server.login(username, password)
|
||||
|
||||
for email in emails:
|
||||
try:
|
||||
|
||||
@@ -58,6 +58,14 @@ SMTP_SETTINGS = {
|
||||
"recipients": normalize_recipients(os.getenv("SMTP_RECIPIENTS")),
|
||||
}
|
||||
|
||||
EMAIL_NOTIFY_CONTACT_FORM = os.getenv("EMAIL_NOTIFY_CONTACT_FORM", "true").lower() in {
|
||||
"1", "true", "yes"}
|
||||
EMAIL_NOTIFY_NEWSLETTER_SIGNUPS = os.getenv("EMAIL_NOTIFY_NEWSLETTER_SIGNUPS", "false").lower() in {
|
||||
"1", "true", "yes"}
|
||||
|
||||
SMTP_SETTINGS["notify_contact_form"] = EMAIL_NOTIFY_CONTACT_FORM
|
||||
SMTP_SETTINGS["notify_newsletter_signups"] = EMAIL_NOTIFY_NEWSLETTER_SIGNUPS
|
||||
|
||||
if not SMTP_SETTINGS["sender"] and SMTP_SETTINGS["username"]:
|
||||
SMTP_SETTINGS["sender"] = SMTP_SETTINGS["username"]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user