feat(newsletter): Add subscription confirmation email functionality
- Implemented `send_subscription_confirmation` function to send a confirmation email upon subscription. - Added a default HTML template for the confirmation email in settings. - Updated the newsletter management page to handle subscription and unsubscription actions. - Created new templates for embedding contact and newsletter forms. - Added styles for the new templates and unified CSS styles across the application. - Updated tests to reflect changes in the newsletter management API endpoints.
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from flask import Flask
|
||||
|
||||
from . import admin, auth, contact, monitoring, newsletter
|
||||
from . import admin, auth, contact, embed, monitoring, newsletter
|
||||
|
||||
|
||||
def register_blueprints(app: Flask) -> None:
|
||||
@@ -13,3 +13,4 @@ def register_blueprints(app: Flask) -> None:
|
||||
app.register_blueprint(monitoring.bp)
|
||||
app.register_blueprint(auth.bp)
|
||||
app.register_blueprint(admin.bp)
|
||||
app.register_blueprint(embed.bp)
|
||||
|
||||
@@ -82,6 +82,13 @@ def submissions():
|
||||
return render_template("admin_submissions.html")
|
||||
|
||||
|
||||
@bp.route("/embeds")
|
||||
@auth.login_required
|
||||
def embeds():
|
||||
"""Display embeddable forms management page."""
|
||||
return render_template("admin_embeds.html")
|
||||
|
||||
|
||||
@bp.route("/api/settings", methods=["GET"])
|
||||
@auth.login_required
|
||||
def get_settings_api():
|
||||
|
||||
26
server/routes/embed.py
Normal file
26
server/routes/embed.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Embeddable forms routes."""
|
||||
from __future__ import annotations
|
||||
|
||||
from flask import Blueprint, render_template, send_from_directory
|
||||
import os
|
||||
|
||||
bp = Blueprint("embed", __name__)
|
||||
|
||||
|
||||
@bp.route("/embed/contact", methods=["GET"])
|
||||
def contact_form():
|
||||
"""Serve the embeddable contact form."""
|
||||
return render_template("embed_contact.html")
|
||||
|
||||
|
||||
@bp.route("/embed/newsletter", methods=["GET"])
|
||||
def newsletter_form():
|
||||
"""Serve the embeddable newsletter subscription form."""
|
||||
return render_template("embed_newsletter.html")
|
||||
|
||||
|
||||
@bp.route("/static/css/styles.css", methods=["GET"])
|
||||
def serve_css():
|
||||
"""Serve the unified CSS file."""
|
||||
static_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'static')
|
||||
return send_from_directory(static_dir, 'css/styles.css')
|
||||
@@ -7,10 +7,10 @@ from flask import Blueprint, jsonify, request, render_template
|
||||
|
||||
from ..services import newsletter
|
||||
|
||||
bp = Blueprint("newsletter", __name__, url_prefix="/api")
|
||||
bp = Blueprint("newsletter", __name__)
|
||||
|
||||
|
||||
@bp.route("/newsletter", methods=["POST"])
|
||||
@bp.route("/api/newsletter", methods=["POST"])
|
||||
def subscribe():
|
||||
payload = request.form or request.get_json(silent=True) or {}
|
||||
email = (payload.get("email") or "").strip()
|
||||
@@ -29,10 +29,16 @@ def subscribe():
|
||||
return jsonify({"status": "error", "message": "Email is already subscribed."}), 409
|
||||
|
||||
logging.info("New newsletter subscription: %s", email)
|
||||
|
||||
# Send confirmation email
|
||||
email_sent = newsletter.send_subscription_confirmation(email)
|
||||
if not email_sent:
|
||||
logging.warning("Confirmation email not sent for %s", email)
|
||||
|
||||
return jsonify({"status": "ok", "message": "Subscribed successfully."}), 201
|
||||
|
||||
|
||||
@bp.route("/newsletter", methods=["DELETE"])
|
||||
@bp.route("/api/newsletter", methods=["DELETE"])
|
||||
def unsubscribe():
|
||||
payload = request.form or request.get_json(silent=True) or {}
|
||||
email = (payload.get("email") or "").strip()
|
||||
@@ -55,7 +61,7 @@ def unsubscribe():
|
||||
return jsonify({"status": "ok", "message": "Unsubscribed successfully."}), 200
|
||||
|
||||
|
||||
@bp.route("/newsletter", methods=["PUT"])
|
||||
@bp.route("/api/newsletter", methods=["PUT"])
|
||||
def update_subscription():
|
||||
payload = request.form or request.get_json(silent=True) or {}
|
||||
old_email = (payload.get("old_email") or "").strip()
|
||||
@@ -104,8 +110,7 @@ def manage_subscription():
|
||||
elif action == "unsubscribe":
|
||||
deleted = newsletter.unsubscribe(email)
|
||||
if deleted:
|
||||
message = "Successfully unsubscribed from newsletter."
|
||||
message_type = "success"
|
||||
return render_template("unsubscribe_confirmation.html")
|
||||
else:
|
||||
message = "This email is not currently subscribed."
|
||||
message_type = "info"
|
||||
@@ -131,3 +136,9 @@ def manage_subscription():
|
||||
message_type = "error"
|
||||
|
||||
return render_template("newsletter_manage.html", message=message, message_type=message_type)
|
||||
|
||||
|
||||
@bp.route("/newsletter/unsubscribe/confirmation", methods=["GET"])
|
||||
def unsubscribe_confirmation():
|
||||
"""Display newsletter unsubscription confirmation page."""
|
||||
return render_template("unsubscribe_confirmation.html")
|
||||
|
||||
@@ -28,6 +28,61 @@ def update_email(old_email: str, new_email: str) -> bool:
|
||||
return update_subscriber(old_email, new_email)
|
||||
|
||||
|
||||
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"]:
|
||||
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()
|
||||
|
||||
try:
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
sender = settings.SMTP_SETTINGS["sender"] or "noreply@example.com"
|
||||
|
||||
# Create message
|
||||
msg = MIMEMultipart('alternative')
|
||||
msg['Subject'] = "Newsletter Subscription Confirmation"
|
||||
msg['From'] = sender
|
||||
msg['To'] = email
|
||||
|
||||
# Add HTML content
|
||||
html_part = MIMEText(template, 'html')
|
||||
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"]:
|
||||
server.starttls()
|
||||
if settings.SMTP_SETTINGS["username"]:
|
||||
server.login(
|
||||
settings.SMTP_SETTINGS["username"], settings.SMTP_SETTINGS["password"] or "")
|
||||
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)
|
||||
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
|
||||
|
||||
@@ -63,3 +63,14 @@ if not SMTP_SETTINGS["sender"] and SMTP_SETTINGS["username"]:
|
||||
|
||||
ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin")
|
||||
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin")
|
||||
|
||||
NEWSLETTER_CONFIRMATION_TEMPLATE = os.getenv("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()
|
||||
|
||||
Reference in New Issue
Block a user