Files
office_translator/services/email_service.py
Sepehr Ramezani 2f7347b4db
Some checks failed
Build and Deploy / Backend Tests (push) Has been cancelled
Build and Deploy / Frontend Build Check (push) Has been cancelled
Build and Deploy / Build Docker Images (push) Has been cancelled
Build and Deploy / Deploy to Server (push) Has been cancelled
feat: fix registration 500, add forgot-password flow, frontend validation
- Fix MissingGreenlet: sync_engine now uses psycopg2 instead of asyncpg
- Fix bcrypt/passlib compat: pin bcrypt<4.1 in requirements
- Fix legacy password_hash NOT NULL: alter column to nullable in migration
- Add frontend password validation (uppercase + lowercase + digit)
- Add forgot-password and reset-password backend endpoints
- Add forgot-password and reset-password frontend pages
- Add email_service.py (SMTP via admin settings)
- Add reset_token/reset_token_expires columns to User model
- Migrate legacy JSON-only users to DB on password reset request
- Mount data/ volume in docker-compose.local.yml for persistence
- Add production deployment config (Dockerfile, nginx, deploy.sh)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 16:23:51 +02:00

120 lines
4.0 KiB
Python

"""
Email service for sending transactional emails via SMTP.
Supports both sync (smtplib) and async (aiosmtplib) sending.
Configuration is resolved from settings JSON file, then env vars.
"""
import os
import logging
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from typing import Optional
logger = logging.getLogger(__name__)
def _get_smtp_config() -> dict:
"""Resolve SMTP config from admin settings file then env vars."""
config = {}
# Read from admin provider settings (same file as /admin/settings page)
try:
import json
from pathlib import Path
settings_path = Path("data/provider_settings.json")
if settings_path.exists():
with open(settings_path, "r") as f:
settings = json.load(f)
smtp = settings.get("smtp", {})
if smtp and smtp.get("host"):
config["host"] = smtp.get("host", "")
config["port"] = smtp.get("port", 587)
config["user"] = smtp.get("username", "")
config["password"] = smtp.get("password", "")
config["from_email"] = smtp.get("from_email", "") or smtp.get("username", "")
config["use_tls"] = smtp.get("use_tls", True)
except Exception:
pass
# Env vars fill gaps
config.setdefault("host", os.getenv("SMTP_HOST", ""))
config.setdefault("port", int(os.getenv("SMTP_PORT", "587")))
config.setdefault("user", os.getenv("SMTP_USER", os.getenv("SMTP_USERNAME", "")))
config.setdefault("password", os.getenv("SMTP_PASSWORD", ""))
config.setdefault("from_email", os.getenv("SMTP_FROM_EMAIL", os.getenv("SMTP_USER", os.getenv("SMTP_USERNAME", ""))))
config.setdefault("use_tls", os.getenv("SMTP_USE_TLS", "true").lower() == "true")
return config
def is_smtp_configured() -> bool:
"""Check if SMTP is properly configured."""
cfg = _get_smtp_config()
return bool(cfg.get("host") and cfg.get("user"))
def send_email(to: str, subject: str, body: str) -> bool:
"""Send an email via SMTP (sync). Returns True if sent successfully."""
cfg = _get_smtp_config()
if not cfg.get("host"):
logger.warning("SMTP not configured, email not sent")
return False
try:
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = cfg["from_email"]
msg["To"] = to
msg.attach(MIMEText(body, "html"))
port = int(cfg.get("port", 587))
use_tls = cfg.get("use_tls", True)
if use_tls:
server = smtplib.SMTP(cfg["host"], port)
server.starttls()
else:
server = smtplib.SMTP(cfg["host"], port)
if cfg.get("user") and cfg.get("password"):
server.login(cfg["user"], cfg["password"])
server.sendmail(cfg["from_email"], [to], msg.as_string())
server.quit()
logger.info(f"Email sent to {to}: {subject}")
return True
except Exception as e:
logger.error(f"Failed to send email to {to}: {e}")
return False
async def send_email_async(to: str, subject: str, body: str) -> bool:
"""Send an email via SMTP (async). Returns True if sent successfully."""
cfg = _get_smtp_config()
if not cfg.get("host"):
logger.warning("SMTP not configured, email not sent")
return False
try:
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = cfg["from_email"]
msg["To"] = to
msg.attach(MIMEText(body, "html"))
port = int(cfg.get("port", 587))
# Use sync smtplib inside a thread for reliability (aiosmtplib has quirks with raw messages)
import asyncio
return await asyncio.get_event_loop().run_in_executor(
None, send_email, to, subject, body
)
except Exception as e:
logger.error(f"Failed to send email (async) to {to}: {e}")
return False