
### 📋 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.
306 lines
11 KiB
Python
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
|