Files
roa2web-service-auto/backend/modules/telegram/utils/email_service.py
Claude Agent 30f55cf18b feat: Add A-Z filter for clients/suppliers in Telegram bot
- Add A-Z alphabetical filter keyboard for clients and suppliers lists
  (same pattern as company selection, without emoji)
- Increase clients/suppliers list pagination from 10 to 20 items per page
- Remove emoji from company A-Z filter button for consistency
- Add 6 new callback handlers: clients_alpha_menu, clients_alpha:LETTER,
  clients_alpha_page:PAGE:LETTER, and supplier equivalents
- Dashboard service and models updates
- Telegram bot: email handlers, auth, DB operations, internal API improvements
- Frontend: dashboard cards updates (CashFlow, Clienti, Furnizori, Treasury)
- Frontend: SolduriCompactCard and CollapsibleCard improvements
- DashboardView enhancements
- start.sh and run-with-restart.sh script updates
- IIS web.config and service worker updates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 14:34:15 +00:00

142 lines
4.7 KiB
Python

"""
Async SMTP Email Service with retry logic and proper error handling
"""
import aiosmtplib
from email.message import EmailMessage
import os
import logging
from typing import Optional
import asyncio
logger = logging.getLogger(__name__)
class EmailService:
"""Async SMTP client for sending authentication codes"""
def __init__(self):
self.smtp_host = os.getenv("SMTP_HOST", "mail.romfast.ro")
self.smtp_port = int(os.getenv("SMTP_PORT", "587"))
self.smtp_user = os.getenv("SMTP_USER")
self.smtp_password = os.getenv("SMTP_PASSWORD")
self.from_email = os.getenv("SMTP_FROM_EMAIL")
self.from_name = os.getenv("SMTP_FROM_NAME", "ROA2WEB")
self.use_tls = os.getenv("SMTP_USE_TLS", "true").lower() == "true"
# Retry configuration
self.max_retries = int(os.getenv("EMAIL_MAX_RETRIES", "3"))
self.retry_delay = float(os.getenv("EMAIL_RETRY_DELAY", "2.0")) # seconds
# Validate required config
if not all([self.smtp_user, self.smtp_password, self.from_email]):
raise ValueError("SMTP configuration incomplete. Check .env file.")
async def send_auth_code(
self,
to_email: str,
code: str,
username: str
) -> bool:
"""
Send authentication code via email with retry logic
Args:
to_email: Recipient email address
code: 6-digit authentication code
username: Oracle username for personalization
Returns:
True if email sent successfully (after retries if needed)
Raises:
No exceptions - returns False on all failures
"""
subject = "Autentificare ROA2WEB"
text_body = self._create_email_template(code, username)
for attempt in range(1, self.max_retries + 1):
try:
await self._send_email(to_email, subject, text_body)
logger.info(
f"[EMAIL] ✅ Sent auth code to {to_email} "
f"(attempt {attempt}/{self.max_retries}) via {self.smtp_host}:{self.smtp_port}"
)
return True
except aiosmtplib.SMTPException as e:
logger.error(
f"[EMAIL] ❌ Attempt {attempt}/{self.max_retries} failed for {to_email}: "
f"{type(e).__name__}: {e}"
)
if attempt < self.max_retries:
# Exponential backoff: 2s, 4s, 8s
delay = self.retry_delay * (2 ** (attempt - 1))
logger.info(f"[EMAIL] Retrying in {delay}s...")
await asyncio.sleep(delay)
else:
logger.error(f"[EMAIL] ❌ All {self.max_retries} attempts failed for {to_email}")
except Exception as e:
logger.error(f"[EMAIL] ❌ Unexpected error on attempt {attempt}/{self.max_retries} for {to_email}: {type(e).__name__}: {e}", exc_info=True)
return False
return False
async def _send_email(
self,
to_email: str,
subject: str,
text_body: str
) -> None:
"""
Internal async SMTP sender (plain text to avoid spam filters)
Raises:
aiosmtplib.SMTPException: On SMTP errors
"""
message = EmailMessage()
message["From"] = f"{self.from_name} <{self.from_email}>"
message["To"] = to_email
message["Subject"] = subject
message.set_content(text_body)
smtp = aiosmtplib.SMTP(
hostname=self.smtp_host,
port=self.smtp_port,
start_tls=self.use_tls,
timeout=30
)
try:
await smtp.connect()
await smtp.login(self.smtp_user, self.smtp_password)
await smtp.send_message(message)
finally:
try:
await smtp.quit()
except:
pass
def _create_email_template(self, code: str, username: str) -> str:
"""Generate plain text email body (HTML blocked by spam filters)"""
return (
f"Codul tau de autentificare ROA2WEB:\n\n"
f" {code}\n\n"
f"Introdu acest cod in Telegram. Expira in 5 minute.\n\n"
f"---\n"
f"Solicitat pentru: {username}\n"
f"Daca nu ai initiat aceasta autentificare, ignora acest email."
)
# Singleton instance
_email_service: Optional[EmailService] = None
def get_email_service() -> EmailService:
"""Get or create singleton email service instance"""
global _email_service
if _email_service is None:
_email_service = EmailService()
return _email_service