nakama/moderation/backend-py/utils/email_service.py

318 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Email sending utilities with support for AWS SES and Yandex SMTP
"""
import smtplib
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Optional
from config import settings
logger = logging.getLogger(__name__)
# Try to import boto3 for AWS SES
try:
import boto3
from botocore.exceptions import ClientError, BotoCoreError
BOTO3_AVAILABLE = True
except ImportError:
BOTO3_AVAILABLE = False
logger.warning("[Email] boto3 не установлен. AWS SES недоступен. Установите: pip install boto3")
def generate_verification_email(code: str) -> str:
"""Generate HTML email with verification code"""
return f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.code {{ font-size: 32px; font-weight: bold; color: #007bff;
text-align: center; padding: 20px; background: #f8f9fa;
border-radius: 8px; margin: 20px 0; letter-spacing: 8px; }}
.footer {{ margin-top: 30px; font-size: 12px; color: #666; }}
</style>
</head>
<body>
<div class="container">
<h1>Код подтверждения</h1>
<p>Ваш код для регистрации в Nakama:</p>
<div class="code">{code}</div>
<p>Код действителен в течение 15 минут.</p>
<div class="footer">
<p>Если вы не запрашивали этот код, просто проигнорируйте это письмо.</p>
</div>
</div>
</body>
</html>
"""
async def send_email_aws_ses(to: str, subject: str, html: str, text: Optional[str] = None):
"""Send email via AWS SES or Yandex Cloud Postbox (SESv2)"""
if not BOTO3_AVAILABLE:
raise ValueError("boto3 не установлен. Установите: pip install boto3")
if not settings.AWS_SES_ACCESS_KEY_ID or not settings.AWS_SES_SECRET_ACCESS_KEY:
raise ValueError("AWS_SES_ACCESS_KEY_ID и AWS_SES_SECRET_ACCESS_KEY должны быть установлены в .env")
try:
# Check if using Yandex Cloud Postbox (has custom endpoint)
is_yandex_cloud = bool(settings.AWS_SES_ENDPOINT_URL and 'yandex' in settings.AWS_SES_ENDPOINT_URL.lower())
# Create client config
client_config = {
'aws_access_key_id': settings.AWS_SES_ACCESS_KEY_ID,
'aws_secret_access_key': settings.AWS_SES_SECRET_ACCESS_KEY,
'region_name': settings.AWS_SES_REGION
}
# Add custom endpoint if specified (for Yandex Cloud Postbox)
if settings.AWS_SES_ENDPOINT_URL:
client_config['endpoint_url'] = settings.AWS_SES_ENDPOINT_URL
# Yandex Cloud Postbox uses SESv2 API
# Also check if region is ru-central1 (Yandex Cloud region)
if is_yandex_cloud or settings.AWS_SES_REGION == 'ru-central1':
endpoint_url = settings.AWS_SES_ENDPOINT_URL or 'https://postbox.cloud.yandex.net'
logger.info(f"[Email] Использование Yandex Cloud Postbox (SESv2 API): {endpoint_url}")
# Override endpoint if not set
if not settings.AWS_SES_ENDPOINT_URL:
client_config['endpoint_url'] = endpoint_url
sesv2_client = boto3.client('sesv2', **client_config)
# Prepare email for SESv2
destination = {'ToAddresses': [to]}
content = {
'Simple': {
'Subject': {
'Data': subject,
'Charset': 'UTF-8'
},
'Body': {
'Html': {
'Data': html,
'Charset': 'UTF-8'
},
'Text': {
'Data': text or html.replace('<[^>]*>', ''),
'Charset': 'UTF-8'
}
}
}
}
# Send email via SESv2
response = sesv2_client.send_email(
FromEmailAddress=settings.EMAIL_FROM,
Destination=destination,
Content=content
)
logger.info(f"✅ Email отправлен через Yandex Cloud Postbox на {to}, MessageId: {response.get('MessageId')}")
return {"success": True, "to": to, "messageId": response.get('MessageId')}
else:
# Standard AWS SES (SESv1 API)
logger.info(f"[Email] Использование AWS SES (SESv1 API): {settings.AWS_SES_REGION}")
ses_client = boto3.client('ses', **client_config)
# Prepare email for SESv1
destination = {'ToAddresses': [to]}
message = {
'Subject': {'Data': subject, 'Charset': 'UTF-8'},
'Body': {
'Html': {'Data': html, 'Charset': 'UTF-8'},
'Text': {'Data': text or html.replace('<[^>]*>', ''), 'Charset': 'UTF-8'}
}
}
# Send email
response = ses_client.send_email(
Source=settings.EMAIL_FROM,
Destination=destination,
Message=message
)
logger.info(f"✅ Email отправлен через AWS SES на {to}, MessageId: {response.get('MessageId')}")
return {"success": True, "to": to, "messageId": response.get('MessageId')}
except ClientError as e:
error_code = e.response.get('Error', {}).get('Code', 'Unknown')
error_msg = e.response.get('Error', {}).get('Message', str(e))
logger.error(f"❌ AWS SES ошибка ({error_code}): {error_msg}")
# More informative error messages
if error_code == 'MessageRejected':
raise ValueError(f"Письмо отклонено: {error_msg}. Проверьте, что адрес {settings.EMAIL_FROM} верифицирован.")
elif error_code == 'InvalidParameterValue':
raise ValueError(f"Неверный параметр: {error_msg}")
elif error_code == 'AccessDenied':
raise ValueError(f"Доступ запрещен: {error_msg}. Проверьте права доступа ключей.")
raise ValueError(f"AWS SES ошибка ({error_code}): {error_msg}")
except Exception as e:
logger.error(f"❌ Ошибка отправки email через AWS SES: {e}")
import traceback
traceback.print_exc()
raise
async def send_email_smtp(to: str, subject: str, html: str, text: Optional[str] = None):
"""Send email via SMTP (Yandex, Gmail, etc.)"""
try:
# Create message
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = settings.EMAIL_FROM
msg['To'] = to
# Add text and HTML parts
if text:
msg.attach(MIMEText(text, 'plain', 'utf-8'))
msg.attach(MIMEText(html, 'html', 'utf-8'))
# Connect and send
# Поддерживаем 'yandex' и 'smtp' как алиасы
email_provider = settings.EMAIL_PROVIDER.lower()
if email_provider in ['yandex', 'smtp']:
logger.info(f"[Email] Отправка через SMTP ({email_provider}): {settings.YANDEX_SMTP_HOST}:{settings.YANDEX_SMTP_PORT}")
if not settings.YANDEX_SMTP_USER or not settings.YANDEX_SMTP_PASSWORD:
raise ValueError("YANDEX_SMTP_USER и YANDEX_SMTP_PASSWORD должны быть установлены в .env")
# Используем SMTP_SSL для порта 465
if settings.YANDEX_SMTP_PORT == 465:
server = smtplib.SMTP_SSL(settings.YANDEX_SMTP_HOST, settings.YANDEX_SMTP_PORT)
else:
server = smtplib.SMTP(settings.YANDEX_SMTP_HOST, settings.YANDEX_SMTP_PORT)
if settings.YANDEX_SMTP_SECURE:
server.starttls()
server.login(settings.YANDEX_SMTP_USER, settings.YANDEX_SMTP_PASSWORD)
server.send_message(msg)
server.quit()
logger.info(f"✅ Email отправлен на {to}")
return {"success": True, "to": to}
else:
raise ValueError(f"Email provider '{settings.EMAIL_PROVIDER}' не поддерживается. Используйте 'aws', 'yandex' или 'smtp'")
except Exception as e:
logger.error(f"❌ Ошибка отправки email: {e}")
# More informative error messages
if "Authentication" in str(e) or "credentials" in str(e).lower():
raise ValueError(
"Неверные учетные данные SMTP. Для Yandex используйте пароль приложения "
"(https://id.yandex.ru/security), а не основной пароль аккаунта."
)
elif "Connection" in str(e):
raise ValueError(
f"Не удалось подключиться к SMTP серверу {settings.YANDEX_SMTP_HOST}:{settings.YANDEX_SMTP_PORT}"
)
raise
async def send_email(to: str, subject: str, html: str, text: Optional[str] = None):
"""Send email using configured provider (AWS SES or SMTP)"""
email_provider = settings.EMAIL_PROVIDER.lower()
logger.info(f"[Email] 🔍 Начало отправки email")
logger.info(f"[Email] Provider из настроек: '{settings.EMAIL_PROVIDER}' (lowercase: '{email_provider}')")
logger.info(f"[Email] To: {to}")
logger.info(f"[Email] From: {settings.EMAIL_FROM}")
logger.info(f"[Email] Subject: {subject}")
if email_provider == 'aws':
logger.info(f"[Email] Выбран AWS SES")
logger.info(f"[Email] AWS_SES_ACCESS_KEY_ID: {'установлен' if settings.AWS_SES_ACCESS_KEY_ID else 'НЕ УСТАНОВЛЕН'}")
logger.info(f"[Email] AWS_SES_SECRET_ACCESS_KEY: {'установлен' if settings.AWS_SES_SECRET_ACCESS_KEY else 'НЕ УСТАНОВЛЕН'}")
logger.info(f"[Email] AWS_SES_REGION: {settings.AWS_SES_REGION}")
logger.info(f"[Email] AWS_SES_ENDPOINT_URL: {settings.AWS_SES_ENDPOINT_URL or 'не установлен'}")
return await send_email_aws_ses(to, subject, html, text)
elif email_provider in ['yandex', 'smtp']:
logger.info(f"[Email] Выбран SMTP ({email_provider})")
logger.info(f"[Email] YANDEX_SMTP_USER: {'установлен' if settings.YANDEX_SMTP_USER else 'НЕ УСТАНОВЛЕН'}")
logger.info(f"[Email] YANDEX_SMTP_PASSWORD: {'установлен' if settings.YANDEX_SMTP_PASSWORD else 'НЕ УСТАНОВЛЕН'}")
logger.info(f"[Email] YANDEX_SMTP_HOST: {settings.YANDEX_SMTP_HOST}")
logger.info(f"[Email] YANDEX_SMTP_PORT: {settings.YANDEX_SMTP_PORT}")
return await send_email_smtp(to, subject, html, text)
else:
logger.error(f"[Email] ❌ Неподдерживаемый провайдер: '{settings.EMAIL_PROVIDER}'")
raise ValueError(f"Email provider '{settings.EMAIL_PROVIDER}' не поддерживается. Используйте 'aws', 'yandex' или 'smtp'")
async def send_verification_code(email: str, code: str):
"""Send verification code to email"""
logger.info(f"[Email] 📧 send_verification_code вызван для {email}")
logger.info(f"[Email] 📧 Проверка настроек перед отправкой:")
logger.info(f"[Email] 📧 EMAIL_PROVIDER (raw): '{settings.EMAIL_PROVIDER}'")
logger.info(f"[Email] 📧 EMAIL_PROVIDER (lower): '{settings.EMAIL_PROVIDER.lower()}'")
logger.info(f"[Email] 📧 EMAIL_FROM: '{settings.EMAIL_FROM}'")
logger.info(f"[Email] 📧 AWS_SES_ACCESS_KEY_ID: {'установлен' if settings.AWS_SES_ACCESS_KEY_ID else 'НЕ УСТАНОВЛЕН'}")
logger.info(f"[Email] 📧 AWS_SES_SECRET_ACCESS_KEY: {'установлен' if settings.AWS_SES_SECRET_ACCESS_KEY else 'НЕ УСТАНОВЛЕН'}")
logger.info(f"[Email] 📧 AWS_SES_REGION: '{settings.AWS_SES_REGION}'")
logger.info(f"[Email] 📧 AWS_SES_ENDPOINT_URL: '{settings.AWS_SES_ENDPOINT_URL}'")
logger.info(f"[Email] 📧 YANDEX_SMTP_USER: {'установлен' if settings.YANDEX_SMTP_USER else 'НЕ УСТАНОВЛЕН'}")
logger.info(f"[Email] 📧 YANDEX_SMTP_PASSWORD: {'установлен' if settings.YANDEX_SMTP_PASSWORD else 'НЕ УСТАНОВЛЕН'}")
subject = "Код подтверждения регистрации - Nakama"
html = generate_verification_email(code)
text = f"Ваш код подтверждения: {code}. Код действителен 15 минут."
return await send_email(email, subject, html, text)
async def send_admin_confirmation_code(code: str, action: str, user_info: dict):
"""Send admin confirmation code to owner email"""
action_text = "добавления админа" if action == "add" else "удаления админа"
subject = f"Код подтверждения {action_text} - Nakama Moderation"
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.code {{ font-size: 32px; font-weight: bold; color: #007bff;
text-align: center; padding: 20px; background: #f8f9fa;
border-radius: 8px; margin: 20px 0; letter-spacing: 8px; }}
.info {{ background: #e7f3ff; padding: 15px; border-radius: 8px; margin: 20px 0; }}
.footer {{ margin-top: 30px; font-size: 12px; color: #666; }}
</style>
</head>
<body>
<div class="container">
<h1>Подтверждение {action_text}</h1>
<p><strong>Пользователь:</strong> @{user_info.get('username', 'не указан')} ({user_info.get('firstName', '')})</p>
{f"<p><strong>Номер админа:</strong> {user_info['adminNumber']}</p>" if 'adminNumber' in user_info else ''}
<div class="info">
<p><strong>Код подтверждения:</strong></p>
<div class="code">{code}</div>
<p>Код действителен в течение 5 минут.</p>
</div>
<div class="footer">
<p>Если вы не запрашивали это подтверждение, проигнорируйте это письмо.</p>
</div>
</div>
</body>
</html>
"""
text = f"""Код подтверждения {action_text}: {code}
Пользователь: @{user_info.get('username', 'не указан')}
Код действителен 5 минут."""
return await send_email(settings.OWNER_EMAIL, subject, html, text)