Files
wfalkowski c177d7981d
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
Refactor backend and add registry build workflow
2026-10-20 16:44:49 +00:00

514 lines
22 KiB
Python

from datetime import datetime
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import logging
import os
import smtplib
logger = logging.getLogger(__name__)
class EmailService:
def __init__(self):
# Load SMTP configuration from environment
self.smtp_host = os.environ.get('SMTP_HOST', '')
self.smtp_port = int(os.environ.get('SMTP_PORT', 587))
self.smtp_user = os.environ.get('SMTP_USER', '')
self.smtp_password = os.environ.get('SMTP_PASSWORD', '')
self.smtp_from = os.environ.get('SMTP_FROM', '')
self.smtp_from_name = os.environ.get('SMTP_FROM_NAME', 'AnchorHost')
# TLS configuration - IMPORTANT!
use_tls_str = os.environ.get('SMTP_USE_TLS', '').lower()
# Default to True for port 587, False for other ports
if use_tls_str:
self.use_tls = use_tls_str in ('true', '1', 'yes', 'on')
else:
# Default: Use TLS on port 587, not on other ports
self.use_tls = (self.smtp_port == 587)
# Base URL for verification links
self.base_url = os.environ.get('BASE_URL', 'http://localhost:8080')
# Validate configuration
if not self.smtp_password:
logger.warning("⚠️ SMTP_PASSWORD not set - email sending will fail!")
logger.info(f"Email Service initialized")
logger.info(f"SMTP Host: {self.smtp_host}:{self.smtp_port}")
logger.info(f"SMTP User: {self.smtp_user}")
logger.info(f"From: {self.smtp_from_name} <{self.smtp_from}>")
logger.info(f"Use TLS: {self.use_tls}")
def send_email(self, to_email: str, subject: str, html_body: str, text_body: str = None) -> bool:
"""
Send an email using SMTP
Args:
to_email: Recipient email address
subject: Email subject
html_body: HTML content of the email
text_body: Plain text fallback (optional)
Returns:
bool: True if email was sent successfully
"""
if not all((self.smtp_host, self.smtp_user, self.smtp_password, self.smtp_from)):
logger.warning("Email not sent because SMTP settings are incomplete")
return False
try:
# Create message
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = f"{self.smtp_from_name} <{self.smtp_from}>"
msg['To'] = to_email
# Add text part (fallback)
if text_body:
part1 = MIMEText(text_body, 'plain')
msg.attach(part1)
# Add HTML part
part2 = MIMEText(html_body, 'html')
msg.attach(part2)
# Connect and send
logger.info(f"Connecting to SMTP server: {self.smtp_host}:{self.smtp_port}")
with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
server.ehlo()
# Check if TLS should be used (ONLY if explicitly enabled)
# Default behavior: Use TLS on port 587 UNLESS SMTP_USE_TLS=false
use_tls = self.smtp_port == 587 and self.use_tls
if use_tls:
logger.info("Starting TLS...")
try:
server.starttls()
server.ehlo()
except smtplib.SMTPNotSupportedError:
logger.warning("TLS not supported by server, continuing without encryption")
else:
logger.info("Skipping TLS (not enabled or not port 587)")
# Login
logger.info(f"Logging in as: {self.smtp_user}")
server.login(self.smtp_user, self.smtp_password)
# Send email
logger.info(f"Sending email to: {to_email}")
server.send_message(msg)
logger.info(f"✅ Email sent successfully to {to_email}")
return True
except smtplib.SMTPAuthenticationError as e:
logger.error(f"❌ SMTP Authentication failed: {e}")
return False
except smtplib.SMTPException as e:
logger.error(f"❌ SMTP error: {e}")
return False
except Exception as e:
logger.error(f"❌ Error sending email: {e}", exc_info=True)
return False
def send_verification_email(self, to_email: str, username: str, token: str) -> bool:
"""
Send email verification email
Args:
to_email: User's email address
username: Username
token: Verification token
Returns:
bool: True if email was sent successfully
"""
verification_url = f"{self.base_url}/verify-email?token={token}"
subject = "Verify your AnchorHost account"
# HTML email template
html_body = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify Your Email</title>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f0f0f;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #0f0f0f; padding: 40px 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #1a1a1a; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);">
<!-- Header -->
<tr>
<td style="background: linear-gradient(135deg, #5a7ca7 0%, #4a6385 100%); padding: 40px 30px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: bold;">
AnchorHost
</h1>
</td>
</tr>
<!-- Body -->
<tr>
<td style="padding: 40px 30px;">
<h2 style="margin: 0 0 20px 0; color: #cbcbcd; font-size: 24px;">
Welcome to AnchorHost, {username}!
</h2>
<p style="margin: 0 0 20px 0; color: #8a8a8a; font-size: 16px; line-height: 1.6;">
Thank you for registering with AnchorHost. To complete your registration and start using our services, please verify your email address.
</p>
<p style="margin: 0 0 30px 0; color: #8a8a8a; font-size: 16px; line-height: 1.6;">
Click the button below to verify your email:
</p>
<!-- Verification Button -->
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 20px 0;">
<a href="{verification_url}" style="display: inline-block; padding: 16px 40px; background: linear-gradient(135deg, #5a7ca7 0%, #4a6385 100%); color: #ffffff; text-decoration: none; font-size: 16px; font-weight: bold; border-radius: 8px; box-shadow: 0 4px 6px rgba(90, 124, 167, 0.3);">
Verify Email Address
</a>
</td>
</tr>
</table>
<p style="margin: 30px 0 20px 0; color: #8a8a8a; font-size: 14px; line-height: 1.6;">
If the button doesn't work, copy and paste this link into your browser:
</p>
<p style="margin: 0 0 30px 0; padding: 15px; background-color: #262626; border-radius: 6px; color: #5a7ca7; font-size: 13px; word-break: break-all; border-left: 3px solid #5a7ca7;">
{verification_url}
</p>
<p style="margin: 0 0 10px 0; color: #8a8a8a; font-size: 14px; line-height: 1.6;">
<strong style="color: #cbcbcd;">Important:</strong> This verification link will expire in 24 hours.
</p>
<p style="margin: 0; color: #8a8a8a; font-size: 14px; line-height: 1.6;">
If you didn't create an account with AnchorHost, please ignore this email.
</p>
</td>
</tr>
<!-- Footer -->
<tr>
<td style="background-color: #161616; padding: 30px; text-align: center; border-top: 1px solid #262626;">
<p style="margin: 0 0 10px 0; color: #8a8a8a; font-size: 12px;">
© 2024 AnchorHost. All rights reserved.
</p>
<p style="margin: 0; color: #8a8a8a; font-size: 12px;">
<a href="https://anchorhost.eu" style="color: #5a7ca7; text-decoration: none;">AnchorHost.eu</a> |
<a href="mailto:support@anchorhost.eu" style="color: #5a7ca7; text-decoration: none;">Support</a>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
"""
# Plain text fallback
text_body = f"""
Welcome to AnchorHost, {username}!
Thank you for registering. Please verify your email address by clicking the link below:
{verification_url}
This link will expire in 24 hours.
If you didn't create an account with AnchorHost, please ignore this email.
---
AnchorHost.eu
"""
return self.send_email(to_email, subject, html_body, text_body)
def send_password_reset_email(self, to_email: str, username: str, token: str) -> bool:
"""
Send password reset email
Args:
to_email: User's email address
username: Username
token: Reset token
Returns:
bool: True if email was sent successfully
"""
reset_url = f"{self.base_url}/reset-password?token={token}"
subject = "Reset your AnchorHost password"
html_body = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reset Your Password</title>
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f0f0f;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #0f0f0f; padding: 40px 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #1a1a1a; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);">
<tr>
<td style="background: linear-gradient(135deg, #8b2d32 0%, #a83a40 100%); padding: 40px 30px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px; font-weight: bold;">
Password Reset
</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 30px;">
<h2 style="margin: 0 0 20px 0; color: #cbcbcd; font-size: 24px;">
Reset your password, {username}
</h2>
<p style="margin: 0 0 20px 0; color: #8a8a8a; font-size: 16px; line-height: 1.6;">
We received a request to reset your password. Click the button below to create a new password:
</p>
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 20px 0;">
<a href="{reset_url}" style="display: inline-block; padding: 16px 40px; background: linear-gradient(135deg, #8b2d32 0%, #a83a40 100%); color: #ffffff; text-decoration: none; font-size: 16px; font-weight: bold; border-radius: 8px;">
Reset Password
</a>
</td>
</tr>
</table>
<p style="margin: 30px 0 20px 0; color: #8a8a8a; font-size: 14px; line-height: 1.6;">
Or copy this link: {reset_url}
</p>
<p style="margin: 0; color: #8a8a8a; font-size: 14px; line-height: 1.6;">
This link expires in 1 hour. If you didn't request this, please ignore this email.
</p>
</td>
</tr>
<tr>
<td style="background-color: #161616; padding: 30px; text-align: center; border-top: 1px solid #262626;">
<p style="margin: 0; color: #8a8a8a; font-size: 12px;">
© 2024 AnchorHost. All rights reserved.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
"""
text_body = f"""
Reset your password, {username}
We received a request to reset your password. Click the link below:
{reset_url}
This link expires in 1 hour. If you didn't request this, ignore this email.
---
AnchorHost.eu
"""
return self.send_email(to_email, subject, html_body, text_body)
def send_welcome_email(self, to_email: str, username: str) -> bool:
"""Send welcome email after successful verification"""
subject = "Welcome to AnchorHost!"
html_body = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif; background-color: #0f0f0f;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #0f0f0f; padding: 40px 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #1a1a1a; border-radius: 12px; overflow: hidden;">
<tr>
<td style="background: linear-gradient(135deg, #5a6b47 0%, #6b7d55 100%); padding: 40px 30px; text-align: center;">
<h1 style="margin: 0; color: #ffffff; font-size: 28px;">🎉 Welcome to AnchorHost!</h1>
</td>
</tr>
<tr>
<td style="padding: 40px 30px;">
<h2 style="margin: 0 0 20px 0; color: #cbcbcd; font-size: 24px;">
You're all set, {username}!
</h2>
<p style="margin: 0 0 20px 0; color: #8a8a8a; font-size: 16px; line-height: 1.6;">
Your email has been verified and your account is now active. You can start deploying servers right away!
</p>
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center" style="padding: 20px 0;">
<a href="https://anchorhost.eu/dashboard" style="display: inline-block; padding: 16px 40px; background: linear-gradient(135deg, #5a6b47 0%, #6b7d55 100%); color: #ffffff; text-decoration: none; font-size: 16px; font-weight: bold; border-radius: 8px;">
Go to Dashboard
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="background-color: #161616; padding: 30px; text-align: center;">
<p style="margin: 0; color: #8a8a8a; font-size: 12px;">
© 2024 AnchorHost. All rights reserved.
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
"""
return self.send_email(to_email, subject, html_body)
def send_monthly_fee_email(self, to_email: str, username: str, server_name: str, amount: int, new_balance: int) -> bool:
"""Send monthly fee confirmation email"""
try:
subject = f"AnchorHost - Monthly Fee for {server_name}"
# Convert credits to the displayed euro amount.
amount_euro = amount / 100
body = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px;">
<h2 style="color: #4a6fa5;">Monthly Server Fee</h2>
<p>Hello {username},</p>
<p>Your monthly fee for server <strong>{server_name}</strong> has been processed.</p>
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h3 style="margin-top: 0;">Payment Details:</h3>
<p><strong>Server:</strong> {server_name}</p>
<p><strong>Amount:</strong> {amount} credits ({amount_euro:.2f} EUR)</p>
<p><strong>Date:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<p><strong>New Balance:</strong> {new_balance} credits</p>
</div>
<p>Your server will remain active as long as you maintain sufficient credits.</p>
<p>You can add more credits at any time from your dashboard.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
<p style="font-size: 12px; color: #666;">
If you have any questions, please contact our support team.
</p>
</div>
</body>
</html>
"""
return self.send_email(to_email, subject, body)
except Exception as e:
logger.error(f"Error sending monthly fee email: {e}")
return False
def send_server_suspended_email(self, to_email: str, username: str, server_name: str, required_amount: int, current_balance: int) -> bool:
"""Send server suspension email due to insufficient credits"""
try:
subject = f"AnchorHost - Server {server_name} Suspended"
required_euro = required_amount / 100
body = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px;">
<h2 style="color: #d9534f;">Server Suspended</h2>
<p>Hello {username},</p>
<p>Your server <strong>{server_name}</strong> has been <strong>suspended</strong> due to insufficient credits.</p>
<div style="background-color: #f2dede; padding: 15px; border-radius: 5px; margin: 20px 0; border: 1px solid #ebccd1;">
<h3 style="margin-top: 0; color: #a94442;">Important Notice:</h3>
<p><strong>Required:</strong> {required_amount} credits ({required_euro:.2f} EUR)</p>
<p><strong>Current Balance:</strong> {current_balance} credits</p>
<p><strong>Missing:</strong> {required_amount - current_balance} credits</p>
</div>
<p>To reactivate your server:</p>
<ol>
<li>Log in to your AnchorHost account</li>
<li>Add credits to your account (minimum {required_amount - current_balance} credits)</li>
<li>Start your server from the dashboard</li>
</ol>
<div style="text-align: center; margin: 30px 0;">
<a href="https://your-domain.com/dashboard" style="background-color: #5a7ca7; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;">
Add Credits Now
</a>
</div>
<p><strong>Note:</strong> Your server data is preserved for 30 days. After this period, it may be permanently deleted.</p>
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
<p style="font-size: 12px; color: #666;">
If you believe this is an error, please contact our support team immediately.
</p>
</div>
</body>
</html>
"""
return self.send_email(to_email, subject, body)
except Exception as e:
logger.error(f"Error sending suspension email: {e}")
return False
# Initialize global email service instance
email_service = None
def init_email_service():
"""Initialize the email service"""
global email_service
try:
email_service = EmailService()
logger.info("✅ Email service initialized")
return email_service
except Exception as e:
logger.error(f"❌ Failed to initialize email service: {e}")
return None
def get_email_service():
"""Get the email service instance"""
global email_service
if email_service is None:
return init_email_service()
return email_service