- 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>
142 lines
4.7 KiB
Python
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
|