This commit is contained in:
Iliyan Angelov
2025-11-16 20:05:08 +02:00
parent 98ccd5b6ff
commit 48353cde9c
118 changed files with 9488 additions and 1336 deletions

View File

@@ -0,0 +1,261 @@
"""
Email templates for various notifications
"""
from datetime import datetime
from typing import Optional
def get_base_template(content: str, title: str = "Hotel Booking") -> str:
"""Base HTML email template"""
return f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #f4f4f4;">
<table role="presentation" style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 20px 0; text-align: center; background-color: #4F46E5;">
<h1 style="color: #ffffff; margin: 0;">Hotel Booking</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 20px; background-color: #ffffff;">
<table role="presentation" style="width: 100%; max-width: 600px; margin: 0 auto;">
<tr>
<td>
{content}
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding: 20px; text-align: center; background-color: #f4f4f4; color: #666666; font-size: 12px;">
<p style="margin: 0;">This is an automated email. Please do not reply.</p>
<p style="margin: 5px 0 0 0;"{datetime.now().year} Hotel Booking. All rights reserved.</p>
</td>
</tr>
</table>
</body>
</html>
"""
def welcome_email_template(name: str, email: str, client_url: str) -> str:
"""Welcome email template for new registrations"""
content = f"""
<h2 style="color: #4F46E5; margin-top: 0;">Welcome {name}!</h2>
<p>Thank you for registering an account at <strong>Hotel Booking</strong>.</p>
<p>Your account has been successfully created with email: <strong>{email}</strong></p>
<div style="background-color: #F3F4F6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<p style="margin: 0;"><strong>You can:</strong></p>
<ul style="margin-top: 10px;">
<li>Search and book hotel rooms</li>
<li>Manage your bookings</li>
<li>Update your personal information</li>
</ul>
</div>
<p style="text-align: center; margin-top: 30px;">
<a href="{client_url}/login" style="background-color: #4F46E5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
Login Now
</a>
</p>
"""
return get_base_template(content, "Welcome to Hotel Booking")
def password_reset_email_template(reset_url: str) -> str:
"""Password reset email template"""
content = f"""
<h2 style="color: #4F46E5; margin-top: 0;">Password Reset Request</h2>
<p>You (or someone) has requested to reset your password.</p>
<p>Click the link below to reset your password. This link will expire in 1 hour:</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{reset_url}" style="background-color: #4F46E5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
Reset Password
</a>
</p>
<p style="color: #666666; font-size: 14px;">If you did not request this, please ignore this email.</p>
"""
return get_base_template(content, "Password Reset")
def password_changed_email_template(email: str) -> str:
"""Password changed confirmation email template"""
content = f"""
<h2 style="color: #4F46E5; margin-top: 0;">Password Changed Successfully</h2>
<p>The password for account <strong>{email}</strong> has been changed successfully.</p>
<p>If you did not make this change, please contact our support team immediately.</p>
"""
return get_base_template(content, "Password Changed")
def booking_confirmation_email_template(
booking_number: str,
guest_name: str,
room_number: str,
room_type: str,
check_in: str,
check_out: str,
num_guests: int,
total_price: float,
requires_deposit: bool,
deposit_amount: Optional[float] = None,
client_url: str = "http://localhost:5173"
) -> str:
"""Booking confirmation email template"""
deposit_info = ""
if requires_deposit and deposit_amount:
deposit_info = f"""
<div style="background-color: #FEF3C7; border-left: 4px solid #F59E0B; padding: 15px; margin: 20px 0; border-radius: 4px;">
<p style="margin: 0; font-weight: bold; color: #92400E;">⚠️ Deposit Required</p>
<p style="margin: 5px 0 0 0; color: #78350F;">Please pay a deposit of <strong>€{deposit_amount:.2f}</strong> to confirm your booking.</p>
<p style="margin: 5px 0 0 0; color: #78350F;">Your booking will be confirmed once the deposit is received.</p>
</div>
"""
content = f"""
<h2 style="color: #4F46E5; margin-top: 0;">Booking Confirmation</h2>
<p>Dear {guest_name},</p>
<p>Thank you for your booking! We have received your reservation request.</p>
<div style="background-color: #F3F4F6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3 style="margin-top: 0; color: #1F2937;">Booking Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #6B7280; width: 40%;">Booking Number:</td>
<td style="padding: 8px 0; font-weight: bold; color: #1F2937;">{booking_number}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6B7280;">Room:</td>
<td style="padding: 8px 0; color: #1F2937;">{room_type} - Room {room_number}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6B7280;">Check-in:</td>
<td style="padding: 8px 0; color: #1F2937;">{check_in}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6B7280;">Check-out:</td>
<td style="padding: 8px 0; color: #1F2937;">{check_out}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6B7280;">Guests:</td>
<td style="padding: 8px 0; color: #1F2937;">{num_guests}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6B7280;">Total Price:</td>
<td style="padding: 8px 0; font-weight: bold; color: #1F2937;">€{total_price:.2f}</td>
</tr>
</table>
</div>
{deposit_info}
<p style="text-align: center; margin-top: 30px;">
<a href="{client_url}/bookings/{booking_number}" style="background-color: #4F46E5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
View Booking Details
</a>
</p>
"""
return get_base_template(content, "Booking Confirmation")
def payment_confirmation_email_template(
booking_number: str,
guest_name: str,
amount: float,
payment_method: str,
transaction_id: Optional[str] = None,
client_url: str = "http://localhost:5173"
) -> str:
"""Payment confirmation email template"""
transaction_info = ""
if transaction_id:
transaction_info = f"""
<tr>
<td style="padding: 8px 0; color: #6B7280;">Transaction ID:</td>
<td style="padding: 8px 0; color: #1F2937; font-family: monospace;">{transaction_id}</td>
</tr>
"""
content = f"""
<h2 style="color: #10B981; margin-top: 0;">Payment Received</h2>
<p>Dear {guest_name},</p>
<p>We have successfully received your payment for booking <strong>{booking_number}</strong>.</p>
<div style="background-color: #ECFDF5; border-left: 4px solid #10B981; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3 style="margin-top: 0; color: #065F46;">Payment Details</h3>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #6B7280; width: 40%;">Booking Number:</td>
<td style="padding: 8px 0; font-weight: bold; color: #1F2937;">{booking_number}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6B7280;">Amount:</td>
<td style="padding: 8px 0; font-weight: bold; color: #065F46; font-size: 18px;">€{amount:.2f}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6B7280;">Payment Method:</td>
<td style="padding: 8px 0; color: #1F2937;">{payment_method}</td>
</tr>
{transaction_info}
</table>
</div>
<p>Your booking is now confirmed. We look forward to hosting you!</p>
<p style="text-align: center; margin-top: 30px;">
<a href="{client_url}/bookings/{booking_number}" style="background-color: #10B981; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
View Booking
</a>
</p>
"""
return get_base_template(content, "Payment Confirmation")
def booking_status_changed_email_template(
booking_number: str,
guest_name: str,
status: str,
client_url: str = "http://localhost:5173"
) -> str:
"""Booking status change email template"""
status_colors = {
"confirmed": ("#10B981", "Confirmed"),
"cancelled": ("#EF4444", "Cancelled"),
"checked_in": ("#3B82F6", "Checked In"),
"checked_out": ("#8B5CF6", "Checked Out"),
}
color, status_text = status_colors.get(status.lower(), ("#6B7280", status.title()))
content = f"""
<h2 style="color: {color}; margin-top: 0;">Booking Status Updated</h2>
<p>Dear {guest_name},</p>
<p>Your booking status has been updated.</p>
<div style="background-color: #F3F4F6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="padding: 8px 0; color: #6B7280; width: 40%;">Booking Number:</td>
<td style="padding: 8px 0; font-weight: bold; color: #1F2937;">{booking_number}</td>
</tr>
<tr>
<td style="padding: 8px 0; color: #6B7280;">New Status:</td>
<td style="padding: 8px 0; font-weight: bold; color: {color}; font-size: 18px;">{status_text}</td>
</tr>
</table>
</div>
<p style="text-align: center; margin-top: 30px;">
<a href="{client_url}/bookings/{booking_number}" style="background-color: {color}; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
View Booking
</a>
</p>
"""
return get_base_template(content, f"Booking {status_text}")

View File

@@ -2,47 +2,96 @@ import aiosmtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import os
import logging
from ..config.settings import settings
logger = logging.getLogger(__name__)
async def send_email(to: str, subject: str, html: str = None, text: str = None):
"""
Send email using SMTP
Requires MAIL_HOST, MAIL_USER and MAIL_PASS to be set in env.
Uses settings from config/settings.py with fallback to environment variables
"""
# Require SMTP credentials to be present
mail_host = os.getenv("MAIL_HOST")
mail_user = os.getenv("MAIL_USER")
mail_pass = os.getenv("MAIL_PASS")
try:
# Get SMTP settings from settings.py, fallback to env vars
mail_host = settings.SMTP_HOST or os.getenv("MAIL_HOST")
mail_user = settings.SMTP_USER or os.getenv("MAIL_USER")
mail_pass = settings.SMTP_PASSWORD or os.getenv("MAIL_PASS")
mail_port = settings.SMTP_PORT or int(os.getenv("MAIL_PORT", "587"))
mail_secure = os.getenv("MAIL_SECURE", "false").lower() == "true"
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
# Get from address - prefer settings, then env, then generate from client_url
from_address = settings.SMTP_FROM_EMAIL or os.getenv("MAIL_FROM")
if not from_address:
# Generate from client_url if not set
domain = client_url.replace('https://', '').replace('http://', '').split('/')[0]
from_address = f"no-reply@{domain}"
# Use from name if available
from_name = settings.SMTP_FROM_NAME or "Hotel Booking"
from_header = f"{from_name} <{from_address}>"
if not (mail_host and mail_user and mail_pass):
raise ValueError(
"SMTP mailer not configured. Set MAIL_HOST, MAIL_USER and MAIL_PASS in env."
if not (mail_host and mail_user and mail_pass):
error_msg = "SMTP mailer not configured. Set SMTP_HOST, SMTP_USER and SMTP_PASSWORD in .env file."
logger.error(error_msg)
raise ValueError(error_msg)
# Create message
message = MIMEMultipart("alternative")
message["From"] = from_header
message["To"] = to
message["Subject"] = subject
if text:
message.attach(MIMEText(text, "plain"))
if html:
message.attach(MIMEText(html, "html"))
# If no content provided, add a default text
if not text and not html:
message.attach(MIMEText("", "plain"))
# Determine TLS/SSL settings
# For port 587: use STARTTLS (use_tls=False, start_tls=True)
# For port 465: use SSL/TLS (use_tls=True, start_tls=False)
# For port 25: plain (usually not used for authenticated sending)
if mail_port == 465 or mail_secure:
# SSL/TLS connection (port 465)
use_tls = True
start_tls = False
elif mail_port == 587:
# STARTTLS connection (port 587)
use_tls = False
start_tls = True
else:
# Plain connection (port 25 or other)
use_tls = False
start_tls = False
logger.info(f"Attempting to send email to {to} via {mail_host}:{mail_port} (use_tls: {use_tls}, start_tls: {start_tls})")
# Send email using SMTP client
smtp_client = aiosmtplib.SMTP(
hostname=mail_host,
port=mail_port,
use_tls=use_tls,
start_tls=start_tls,
username=mail_user,
password=mail_pass,
)
mail_port = int(os.getenv("MAIL_PORT", "587"))
mail_secure = os.getenv("MAIL_SECURE", "false").lower() == "true"
client_url = os.getenv("CLIENT_URL", "example.com")
from_address = os.getenv("MAIL_FROM", f"no-reply@{client_url.replace('https://', '').replace('http://', '')}")
# Create message
message = MIMEMultipart("alternative")
message["From"] = from_address
message["To"] = to
message["Subject"] = subject
if text:
message.attach(MIMEText(text, "plain"))
if html:
message.attach(MIMEText(html, "html"))
# Send email
await aiosmtplib.send(
message,
hostname=mail_host,
port=mail_port,
use_tls=not mail_secure and mail_port == 587,
start_tls=not mail_secure and mail_port == 587,
username=mail_user,
password=mail_pass,
)
try:
await smtp_client.connect()
# Authentication happens automatically if username/password are provided in constructor
await smtp_client.send_message(message)
logger.info(f"Email sent successfully to {to}")
finally:
await smtp_client.quit()
except Exception as e:
error_msg = f"Failed to send email to {to}: {type(e).__name__}: {str(e)}"
logger.error(error_msg, exc_info=True)
raise