feat(email): add provider abstraction and HTML templates

Add pluggable email provider system supporting SMTP, Mailgun, and SendGrid
with factory pattern for runtime provider selection. Includes branded HTML
email templates for verification, password reset, MFA notifications, and
organization invites.

Also rebrands all email content from Gatehouse to Secuird, adds email
provider configuration options, and fixes duplicate log handlers in
development mode.
This commit is contained in:
2026-04-04 16:55:00 +10:30
parent d90a06437e
commit 41bbdb4bef
17 changed files with 1068 additions and 76 deletions
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Test script to verify email delivery with HTML templates."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from dotenv import load_dotenv
load_dotenv(".env")
from gatehouse_app import create_app
from gatehouse_app.services.email_provider import EmailProviderFactory, EmailMessage
from gatehouse_app.services import email_templates
def test_html_email():
app = create_app()
print("Testing HTML Email Templates...")
print(f"EMAIL_PROVIDER: {app.config.get('EMAIL_PROVIDER')}")
print(f"MAILGUN_DOMAIN: {app.config.get('MAILGUN_DOMAIN')}")
with app.app_context():
provider = EmailProviderFactory.get_provider()
print(f"Provider class: {provider.__class__.__name__}")
# Test 1: Email Verification
print("\n--- Test 1: Email Verification ---")
html_body = email_templates.build_email_verification_html(
user_name="Cory",
verify_link="https://secuird.tech/verify-email?token=test123",
expiry_hours=24,
)
message = EmailMessage(
to="cory@hawkvelt.id.au",
subject="Verify your Secuird email address",
body="Plain text version: Please verify your email by clicking the link.",
html_body=html_body,
from_address="Secuird <noreply@secuird.tech>",
)
success = provider.send(message)
print(f"Result: {'✅ SUCCESS' if success else '❌ FAILED'}")
# Test 2: Password Reset
print("\n--- Test 2: Password Reset ---")
html_body = email_templates.build_password_reset_html(
user_name="Cory",
reset_link="https://secuird.tech/reset-password?token=test456",
expiry_hours=2,
)
message = EmailMessage(
to="cory@hawkvelt.id.au",
subject="Reset your Secuird password",
body="Plain text version: Reset your password by clicking the link.",
html_body=html_body,
from_address="Secuird <noreply@secuird.tech>",
)
success = provider.send(message)
print(f"Result: {'✅ SUCCESS' if success else '❌ FAILED'}")
# Test 3: MFA Deadline Reminder
print("\n--- Test 3: MFA Deadline Reminder ---")
html_body = email_templates.build_mfa_deadline_reminder_html(
user_name="Cory",
org_name="Acme Corp",
days_remaining=5,
deadline_date="2026-04-09 23:59 UTC",
mfa_methods="Authenticator app (TOTP) or Passkey (WebAuthn)",
setup_link="https://secuird.tech/settings/security",
)
message = EmailMessage(
to="cory@hawkvelt.id.au",
subject="Action Required: MFA enrollment deadline in 5 days",
body="Plain text version: MFA enrollment required.",
html_body=html_body,
from_address="Secuird <noreply@secuird.tech>",
)
success = provider.send(message)
print(f"Result: {'✅ SUCCESS' if success else '❌ FAILED'}")
# Test 4: MFA Suspension
print("\n--- Test 4: MFA Suspension ---")
html_body = email_templates.build_mfa_suspension_html(
user_name="Cory",
org_name="Acme Corp",
mfa_methods="Authenticator app (TOTP) or Passkey (WebAuthn)",
setup_link="https://secuird.tech/settings/security",
)
message = EmailMessage(
to="cory@hawkvelt.id.au",
subject="Account Access Restricted - MFA Enrollment Required",
body="Plain text version: Your account has been suspended.",
html_body=html_body,
from_address="Secuird <noreply@secuird.tech>",
)
success = provider.send(message)
print(f"Result: {'✅ SUCCESS' if success else '❌ FAILED'}")
# Test 5: Organization Invite
print("\n--- Test 5: Organization Invite ---")
html_body = email_templates.build_org_invite_html(
inviter_name="Admin User",
org_name="Acme Corporation",
invite_link="https://secuird.tech/invite?token=test789",
role="Member",
expiry_days=7,
)
message = EmailMessage(
to="cory@hawkvelt.id.au",
subject="You're invited to join Acme Corporation on Secuird",
body="Plain text version: You've been invited to join.",
html_body=html_body,
from_address="Secuird <noreply@secuird.tech>",
)
success = provider.send(message)
print(f"Result: {'✅ SUCCESS' if success else '❌ FAILED'}")
# Test 6: Account Activation
print("\n--- Test 6: Account Activation ---")
html_body = email_templates.build_account_activation_html(
user_name="Cory",
activation_link="https://secuird.tech/activate?code=testabc",
)
message = EmailMessage(
to="cory@hawkvelt.id.au",
subject="Activate your Secuird account",
body="Plain text version: Activate your account.",
html_body=html_body,
from_address="Secuird <noreply@secuird.tech>",
)
success = provider.send(message)
print(f"Result: {'✅ SUCCESS' if success else '❌ FAILED'}")
# Test 7: Email Verification Resend
print("\n--- Test 7: Email Verification Resend ---")
html_body = email_templates.build_email_verification_resend_html(
user_name="Cory",
verify_link="https://secuird.tech/verify-email?token=testxyz",
expiry_hours=24,
)
message = EmailMessage(
to="cory@hawkvelt.id.au",
subject="Verify your Secuird email address",
body="Plain text version: Please verify your email.",
html_body=html_body,
from_address="Secuird <noreply@secuird.tech>",
)
success = provider.send(message)
print(f"Result: {'✅ SUCCESS' if success else '❌ FAILED'}")
print("\n" + "=" * 50)
print("All 7 email templates sent!")
print("=" * 50)
if __name__ == "__main__":
test_html_email()