evo-ai/src/services/email_service.py
OrionDesign 1bcd76595c ⚙️ Fix: importação e seeders automáticos
### 📋 Descrição

Esta PR aborda dois pontos críticos identificados durante a execução do container da aplicação:

####  Correção de importação no `email_service.py`

- Corrigido o caminho da importação:
  - **De:** `from config.settings import settings`
  - **Para:** `from src.config.settings import settings`
- Essa alteração soluciona o erro `ModuleNotFoundError: No module named 'config'`, que impedia a inicialização da aplicação.

####  Execução automática dos seeders via Dockerfile

- Adicionado o script de seeders à sequência de inicialização da aplicação no Dockerfile.
- O comando de inicialização foi alterado para:
  ```bash
  alembic upgrade head && python -m scripts.run_seeders && uvicorn src.main:app --host $HOST --port $PORT
  ```
- Isso garante que os seeders (incluindo o usuário admin) sejam executados automaticamente após as migrações.

---

### 💥 Impacto

- Corrige o erro de importação, permitindo que a aplicação seja iniciada corretamente.
- Automatiza a criação do usuário admin e outros dados iniciais essenciais.
- Melhora a experiência de primeira execução, eliminando etapas manuais.

---

###  Testes realizados

- Verificado que a aplicação inicia corretamente após as alterações.
- Confirmado que os seeders são executados com sucesso, criando o usuário admin e outros dados iniciais conforme esperado.

---

### 📝 Observações

- O novo caminho de importação em `email_service.py` está alinhado com o padrão utilizado nos demais arquivos do projeto.
- Os seeders são executados somente após a conclusão bem-sucedida das migrações do banco de dados.
2025-05-16 01:51:47 -03:00

306 lines
11 KiB
Python

"""
┌──────────────────────────────────────────────────────────────────────────────┐
│ @author: Davidson Gomes │
│ @file: email_service.py │
│ Developed by: Davidson Gomes │
│ Creation date: May 13, 2025 │
│ Contact: contato@evolution-api.com │
├──────────────────────────────────────────────────────────────────────────────┤
│ @copyright © Evolution API 2025. All rights reserved. │
│ Licensed under the Apache License, Version 2.0 │
│ │
│ You may not use this file except in compliance with the License. │
│ You may obtain a copy of the License at │
│ │
│ http://www.apache.org/licenses/LICENSE-2.0 │
│ │
│ Unless required by applicable law or agreed to in writing, software │
│ distributed under the License is distributed on an "AS IS" BASIS, │
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
│ See the License for the specific language governing permissions and │
│ limitations under the License. │
├──────────────────────────────────────────────────────────────────────────────┤
│ @important │
│ For any future changes to the code in this file, it is recommended to │
│ include, together with the modification, the information of the developer │
│ who changed it and the date of modification. │
└──────────────────────────────────────────────────────────────────────────────┘
"""
import sendgrid
from sendgrid.helpers.mail import Mail, Email, To, Content
import logging
from datetime import datetime
from jinja2 import Environment, FileSystemLoader, select_autoescape
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from pathlib import Path
from src.config.settings import settings
logger = logging.getLogger(__name__)
# Configure Jinja2 to load templates
templates_dir = Path(__file__).parent.parent / "templates" / "emails"
os.makedirs(templates_dir, exist_ok=True)
# Configure Jinja2 with the templates directory
env = Environment(
loader=FileSystemLoader(templates_dir),
autoescape=select_autoescape(["html", "xml"]),
)
def _render_template(template_name: str, context: dict) -> str:
"""
Render a template with the provided data
Args:
template_name: Template file name
context: Data to render in the template
Returns:
str: Rendered HTML
"""
try:
template = env.get_template(f"{template_name}.html")
return template.render(**context)
except Exception as e:
logger.error(f"Error rendering template '{template_name}': {str(e)}")
return f"<p>Could not display email content. Please access {context.get('verification_link', '') or context.get('reset_link', '')}</p>"
def _send_email_sendgrid(to_email: str, subject: str, html_content: str) -> bool:
"""
Send an email using SendGrid provider
Args:
to_email: Recipient's email
subject: Email subject
html_content: HTML content of the email
Returns:
bool: True if the email was sent successfully, False otherwise
"""
try:
sg = sendgrid.SendGridAPIClient(api_key=settings.SENDGRID_API_KEY)
from_email = Email(settings.EMAIL_FROM)
to_email = To(to_email)
content = Content("text/html", html_content)
mail = Mail(from_email, to_email, subject, content)
response = sg.client.mail.send.post(request_body=mail.get())
if response.status_code >= 200 and response.status_code < 300:
logger.info(f"Email sent via SendGrid to {to_email}")
return True
else:
logger.error(
f"Failed to send email via SendGrid to {to_email}. Status: {response.status_code}"
)
return False
except Exception as e:
logger.error(f"Error sending email via SendGrid to {to_email}: {str(e)}")
return False
def _send_email_smtp(to_email: str, subject: str, html_content: str) -> bool:
"""
Send an email using SMTP provider
Args:
to_email: Recipient's email
subject: Email subject
html_content: HTML content of the email
Returns:
bool: True if the email was sent successfully, False otherwise
"""
try:
# Create message container
msg = MIMEMultipart('alternative')
msg['Subject'] = subject
msg['From'] = settings.SMTP_FROM or settings.EMAIL_FROM
msg['To'] = to_email
# Attach HTML content
part = MIMEText(html_content, 'html')
msg.attach(part)
# Setup SMTP server
if settings.SMTP_USE_SSL:
server = smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT)
else:
server = smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT)
if settings.SMTP_USE_TLS:
server.starttls()
# Login if credentials are provided
if settings.SMTP_USER and settings.SMTP_PASSWORD:
server.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
# Send email
server.sendmail(
settings.SMTP_FROM or settings.EMAIL_FROM,
to_email,
msg.as_string()
)
server.quit()
logger.info(f"Email sent via SMTP to {to_email}")
return True
except Exception as e:
logger.error(f"Error sending email via SMTP to {to_email}: {str(e)}")
return False
def send_email(to_email: str, subject: str, html_content: str) -> bool:
"""
Send an email using the configured provider
Args:
to_email: Recipient's email
subject: Email subject
html_content: HTML content of the email
Returns:
bool: True if the email was sent successfully, False otherwise
"""
if settings.EMAIL_PROVIDER.lower() == "smtp":
return _send_email_smtp(to_email, subject, html_content)
else: # Default to SendGrid
return _send_email_sendgrid(to_email, subject, html_content)
def send_verification_email(email: str, token: str) -> bool:
"""
Send a verification email to the user
Args:
email: Recipient's email
token: Email verification token
Returns:
bool: True if the email was sent successfully, False otherwise
"""
try:
subject = "Email Verification - Evo AI"
verification_link = f"{settings.APP_URL}/security/verify-email?code={token}"
html_content = _render_template(
"verification_email",
{
"verification_link": verification_link,
"user_name": email.split("@")[0], # Use part of the email as temporary name
"current_year": datetime.now().year,
},
)
return send_email(email, subject, html_content)
except Exception as e:
logger.error(f"Error preparing verification email to {email}: {str(e)}")
return False
def send_password_reset_email(email: str, token: str) -> bool:
"""
Send a password reset email to the user
Args:
email: Recipient's email
token: Password reset token
Returns:
bool: True if the email was sent successfully, False otherwise
"""
try:
subject = "Password Reset - Evo AI"
reset_link = f"{settings.APP_URL}/security/reset-password?token={token}"
html_content = _render_template(
"password_reset",
{
"reset_link": reset_link,
"user_name": email.split("@")[0], # Use part of the email as temporary name
"current_year": datetime.now().year,
},
)
return send_email(email, subject, html_content)
except Exception as e:
logger.error(f"Error preparing password reset email to {email}: {str(e)}")
return False
def send_welcome_email(email: str, user_name: str = None) -> bool:
"""
Send a welcome email to the user after verification
Args:
email: Recipient's email
user_name: User's name (optional)
Returns:
bool: True if the email was sent successfully, False otherwise
"""
try:
subject = "Welcome to Evo AI"
dashboard_link = f"{settings.APP_URL}/dashboard"
html_content = _render_template(
"welcome_email",
{
"dashboard_link": dashboard_link,
"user_name": user_name or email.split("@")[0],
"current_year": datetime.now().year,
},
)
return send_email(email, subject, html_content)
except Exception as e:
logger.error(f"Error preparing welcome email to {email}: {str(e)}")
return False
def send_account_locked_email(
email: str, reset_token: str, failed_attempts: int, time_period: str
) -> bool:
"""
Send an email informing that the account has been locked after login attempts
Args:
email: Recipient's email
reset_token: Token to reset the password
failed_attempts: Number of failed attempts
time_period: Time period of the attempts
Returns:
bool: True if the email was sent successfully, False otherwise
"""
try:
subject = "Security Alert - Account Locked"
reset_link = f"{settings.APP_URL}/security/reset-password?token={reset_token}"
html_content = _render_template(
"account_locked",
{
"reset_link": reset_link,
"user_name": email.split("@")[0],
"failed_attempts": failed_attempts,
"time_period": time_period,
"current_year": datetime.now().year,
},
)
return send_email(email, subject, html_content)
except Exception as e:
logger.error(f"Error preparing account locked email to {email}: {str(e)}")
return False