Files
remnawave-bedolaga-telegram…/app/cabinet/services/email_service.py
T

328 lines
14 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 service for sending verification and password reset emails."""
import logging
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from app.config import settings
logger = logging.getLogger(__name__)
class EmailService:
"""Service for sending emails via SMTP."""
def __init__(self):
self.host = settings.SMTP_HOST
self.port = settings.SMTP_PORT
self.user = settings.SMTP_USER
self.password = settings.SMTP_PASSWORD
self.from_email = settings.get_smtp_from_email()
self.from_name = settings.SMTP_FROM_NAME
self.use_tls = settings.SMTP_USE_TLS
def is_configured(self) -> bool:
"""Check if SMTP is properly configured."""
return settings.is_smtp_configured()
def _get_smtp_connection(self) -> smtplib.SMTP:
"""Create and return SMTP connection."""
smtp = smtplib.SMTP(self.host, self.port)
smtp.ehlo()
if self.use_tls:
smtp.starttls()
smtp.ehlo()
# Only attempt login if credentials are provided AND server supports AUTH
if self.user and self.password:
if smtp.has_extn('auth'):
smtp.login(self.user, self.password)
else:
logger.debug(f'SMTP server {self.host} does not support AUTH, skipping authentication')
return smtp
def send_email(
self,
to_email: str,
subject: str,
body_html: str,
body_text: str | None = None,
) -> bool:
"""
Send an email.
Args:
to_email: Recipient email address
subject: Email subject
body_html: HTML body content
body_text: Plain text body (optional, generated from HTML if not provided)
Returns:
True if email was sent successfully, False otherwise
"""
if not self.is_configured():
logger.warning('SMTP is not configured, cannot send email')
return False
try:
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = f'{self.from_name} <{self.from_email}>'
msg['To'] = to_email
# Plain text version
if body_text is None:
# Simple HTML to text conversion
import re
body_text = re.sub(r'<[^>]+>', '', body_html)
body_text = body_text.replace('&nbsp;', ' ')
body_text = body_text.replace('&amp;', '&')
body_text = body_text.replace('&lt;', '<')
body_text = body_text.replace('&gt;', '>')
part1 = MIMEText(body_text, 'plain', 'utf-8')
part2 = MIMEText(body_html, 'html', 'utf-8')
msg.attach(part1)
msg.attach(part2)
with self._get_smtp_connection() as smtp:
smtp.sendmail(self.from_email, to_email, msg.as_string())
logger.info(f'Email sent successfully to {to_email}')
return True
except Exception as e:
logger.error(f'Failed to send email to {to_email}: {e}')
return False
def send_verification_email(
self,
to_email: str,
verification_token: str,
verification_url: str,
username: str | None = None,
language: str = 'ru',
) -> bool:
"""
Send email verification email.
Args:
to_email: Recipient email address
verification_token: Verification token
verification_url: Base URL for verification (token will be appended)
username: User's name for personalization
language: Language code (ru, en, zh, ua)
Returns:
True if email was sent successfully, False otherwise
"""
full_url = f'{verification_url}?token={verification_token}'
expire_hours = settings.get_cabinet_email_verification_expire_hours()
# Localized content
texts = {
'ru': {
'greeting': f'Здравствуйте{", " + username if username else ""}!',
'subject': 'Подтверждение email адреса',
'intro': 'Спасибо за регистрацию! Пожалуйста, подтвердите ваш email адрес, нажав на кнопку ниже:',
'button': 'Подтвердить email',
'or_copy': 'Или скопируйте и вставьте эту ссылку в браузер:',
'expires': f'Ссылка действительна в течение {expire_hours} часов.',
'ignore': 'Если вы не создавали аккаунт, просто проигнорируйте это письмо.',
'regards': 'С уважением,',
},
'en': {
'greeting': f'Hello{", " + username if username else ""}!',
'subject': 'Verify your email address',
'intro': 'Thank you for registering! Please verify your email address by clicking the button below:',
'button': 'Verify Email',
'or_copy': 'Or copy and paste this link in your browser:',
'expires': f'This link will expire in {expire_hours} hours.',
'ignore': "If you didn't create an account, you can safely ignore this email.",
'regards': 'Best regards,',
},
'zh': {
'greeting': f'您好{", " + username if username else ""}!',
'subject': '验证您的邮箱地址',
'intro': '感谢您的注册!请点击下方按钮验证您的邮箱地址:',
'button': '验证邮箱',
'or_copy': '或将此链接复制并粘贴到浏览器中:',
'expires': f'此链接将在 {expire_hours} 小时后过期。',
'ignore': '如果您没有创建账户,请忽略此邮件。',
'regards': '此致,',
},
'ua': {
'greeting': f'Вітаємо{", " + username if username else ""}!',
'subject': 'Підтвердження email адреси',
'intro': 'Дякуємо за реєстрацію! Будь ласка, підтвердіть вашу email адресу, натиснувши на кнопку нижче:',
'button': 'Підтвердити email',
'or_copy': 'Або скопіюйте та вставте це посилання в браузер:',
'expires': f'Посилання дійсне протягом {expire_hours} годин.',
'ignore': 'Якщо ви не створювали акаунт, просто проігноруйте цей лист.',
'regards': 'З повагою,',
},
}
t = texts.get(language, texts['ru'])
subject = t['subject']
body_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; }}
.button {{
display: inline-block;
padding: 12px 24px;
background-color: #007bff;
color: white !important;
text-decoration: none;
border-radius: 5px;
margin: 20px 0;
}}
.footer {{ margin-top: 30px; font-size: 12px; color: #666; }}
</style>
</head>
<body>
<div class="container">
<h2>{t['greeting']}</h2>
<p>{t['intro']}</p>
<a href="{full_url}" class="button">{t['button']}</a>
<p>{t['or_copy']}</p>
<p><a href="{full_url}">{full_url}</a></p>
<p>{t['expires']}</p>
<p>{t['ignore']}</p>
<div class="footer">
<p>{t['regards']}<br>{self.from_name}</p>
</div>
</div>
</body>
</html>
"""
return self.send_email(to_email, subject, body_html)
def send_password_reset_email(
self,
to_email: str,
reset_token: str,
reset_url: str,
username: str | None = None,
language: str = 'ru',
) -> bool:
"""
Send password reset email.
Args:
to_email: Recipient email address
reset_token: Password reset token
reset_url: Base URL for password reset (token will be appended)
username: User's name for personalization
language: Language code (ru, en, zh, ua)
Returns:
True if email was sent successfully, False otherwise
"""
full_url = f'{reset_url}?token={reset_token}'
expire_hours = settings.get_cabinet_password_reset_expire_hours()
# Localized content
texts = {
'ru': {
'greeting': f'Здравствуйте{", " + username if username else ""}!',
'subject': 'Сброс пароля',
'intro': 'Мы получили запрос на сброс вашего пароля. Нажмите на кнопку ниже, чтобы установить новый пароль:',
'button': 'Сбросить пароль',
'or_copy': 'Или скопируйте и вставьте эту ссылку в браузер:',
'expires': f'Ссылка действительна в течение {expire_hours} часов.',
'warning': 'Если вы не запрашивали сброс пароля, проигнорируйте это письмо или свяжитесь с поддержкой.',
'regards': 'С уважением,',
},
'en': {
'greeting': f'Hello{", " + username if username else ""}!',
'subject': 'Reset your password',
'intro': 'We received a request to reset your password. Click the button below to set a new password:',
'button': 'Reset Password',
'or_copy': 'Or copy and paste this link in your browser:',
'expires': f'This link will expire in {expire_hours} hour(s).',
'warning': "If you didn't request a password reset, please ignore this email or contact support if you're concerned.",
'regards': 'Best regards,',
},
'zh': {
'greeting': f'您好{", " + username if username else ""}!',
'subject': '重置您的密码',
'intro': '我们收到了重置您密码的请求。点击下方按钮设置新密码:',
'button': '重置密码',
'or_copy': '或将此链接复制并粘贴到浏览器中:',
'expires': f'此链接将在 {expire_hours} 小时后过期。',
'warning': '如果您没有请求重置密码,请忽略此邮件或联系客服。',
'regards': '此致,',
},
'ua': {
'greeting': f'Вітаємо{", " + username if username else ""}!',
'subject': 'Скидання пароля',
'intro': 'Ми отримали запит на скидання вашого пароля. Натисніть на кнопку нижче, щоб встановити новий пароль:',
'button': 'Скинути пароль',
'or_copy': 'Або скопіюйте та вставте це посилання в браузер:',
'expires': f'Посилання дійсне протягом {expire_hours} годин.',
'warning': "Якщо ви не запитували скидання пароля, проігноруйте цей лист або зв'яжіться з підтримкою.",
'regards': 'З повагою,',
},
}
t = texts.get(language, texts['ru'])
subject = t['subject']
body_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; }}
.button {{
display: inline-block;
padding: 12px 24px;
background-color: #dc3545;
color: white !important;
text-decoration: none;
border-radius: 5px;
margin: 20px 0;
}}
.footer {{ margin-top: 30px; font-size: 12px; color: #666; }}
.warning {{ color: #dc3545; font-weight: bold; }}
</style>
</head>
<body>
<div class="container">
<h2>{t['greeting']}</h2>
<p>{t['intro']}</p>
<a href="{full_url}" class="button">{t['button']}</a>
<p>{t['or_copy']}</p>
<p><a href="{full_url}">{full_url}</a></p>
<p>{t['expires']}</p>
<p class="warning">{t['warning']}</p>
<div class="footer">
<p>{t['regards']}<br>{self.from_name}</p>
</div>
</div>
</body>
</html>
"""
return self.send_email(to_email, subject, body_html)
# Singleton instance
email_service = EmailService()