From 769f6bf97bf66364c4c38c869b2aef0b021c66cd Mon Sep 17 00:00:00 2001 From: Diwberg de Andrade Pereira Date: Fri, 16 May 2025 10:49:40 -0300 Subject: [PATCH] feat(email): implement SMTP email sending functionality and update environment configuration --- .env.example | 10 ++ src/services/email_service.py | 194 ++++++++++++++++++---------------- 2 files changed, 115 insertions(+), 89 deletions(-) diff --git a/.env.example b/.env.example index 7cd11d9f..14153205 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,16 @@ ENCRYPTION_KEY="your-encryption-key" SENDGRID_API_KEY="your-sendgrid-api-key" EMAIL_FROM="noreply@yourdomain.com" APP_URL="https://yourdomain.com" +# Email provider settings +EMAIL_PROVIDER="smtp" +# SMTP settings +SMTP_HOST="smtp.resend.com" +SMTP_FROM="noreply@exemple.com" +SMTP_USER="resend" +SMTP_PASSWORD="strongpass" +SMTP_PORT="587" +SMTP_USE_TLS=true +SMTP_USE_SSL=false LANGFUSE_PUBLIC_KEY="your-langfuse-public-key" LANGFUSE_SECRET_KEY="your-langfuse-secret-key" diff --git a/src/services/email_service.py b/src/services/email_service.py index 543978c8..cbaad5cf 100644 --- a/src/services/email_service.py +++ b/src/services/email_service.py @@ -34,6 +34,10 @@ from datetime import datetime from jinja2 import Environment, FileSystemLoader, select_autoescape import os from pathlib import Path +# SMTP +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart logger = logging.getLogger(__name__) @@ -66,7 +70,6 @@ def _render_template(template_name: str, context: dict) -> str: logger.error(f"Error rendering template '{template_name}': {str(e)}") return f"

Could not display email content. Please access {context.get('verification_link', '') or context.get('reset_link', '')}

" - def send_verification_email(email: str, token: str) -> bool: """ Send a verification email to the user @@ -79,43 +82,23 @@ def send_verification_email(email: str, token: str) -> bool: bool: True if the email was sent successfully, False otherwise """ try: - sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY")) - from_email = Email(os.getenv("EMAIL_FROM")) - to_email = To(email) subject = "Email Verification - Evo AI" - verification_link = f"{os.getenv('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 + "user_name": email.split("@")[0], "current_year": datetime.now().year, }, ) - 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"Verification email sent to {email}") - return True - else: - logger.error( - f"Failed to send verification email to {email}. Status: {response.status_code}" - ) - return False - + return _send_email(email, subject, html_content) except Exception as e: - logger.error(f"Error sending verification email to {email}: {str(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 @@ -128,43 +111,23 @@ def send_password_reset_email(email: str, token: str) -> bool: bool: True if the email was sent successfully, False otherwise """ try: - sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY")) - from_email = Email(os.getenv("EMAIL_FROM")) - to_email = To(email) subject = "Password Reset - Evo AI" - reset_link = f"{os.getenv('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 + "user_name": email.split("@")[0], "current_year": datetime.now().year, }, ) - 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"Password reset email sent to {email}") - return True - else: - logger.error( - f"Failed to send password reset email to {email}. Status: {response.status_code}" - ) - return False - + return _send_email(email, subject, html_content) except Exception as e: - logger.error(f"Error sending password reset email to {email}: {str(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 @@ -177,11 +140,7 @@ def send_welcome_email(email: str, user_name: str = None) -> bool: bool: True if the email was sent successfully, False otherwise """ try: - sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY")) - from_email = Email(os.getenv("EMAIL_FROM")) - to_email = To(email) subject = "Welcome to Evo AI" - dashboard_link = f"{os.getenv('APP_URL')}/dashboard" html_content = _render_template( @@ -193,25 +152,11 @@ def send_welcome_email(email: str, user_name: str = None) -> bool: }, ) - 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"Welcome email sent to {email}") - return True - else: - logger.error( - f"Failed to send welcome email to {email}. Status: {response.status_code}" - ) - return False - + return _send_email(email, subject, html_content) except Exception as e: - logger.error(f"Error sending welcome email to {email}: {str(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: @@ -228,14 +173,8 @@ def send_account_locked_email( bool: True if the email was sent successfully, False otherwise """ try: - sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY")) - from_email = Email(os.getenv("EMAIL_FROM")) - to_email = To(email) subject = "Security Alert - Account Locked" - - reset_link = ( - f"{os.getenv('APP_URL')}/security/reset-password?token={reset_token}" - ) + reset_link = f"{os.getenv('APP_URL')}/security/reset-password?token={reset_token}" html_content = _render_template( "account_locked", @@ -248,20 +187,97 @@ def send_account_locked_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"Account locked email sent to {email}") - return True - else: - logger.error( - f"Failed to send account locked email to {email}. Status: {response.status_code}" - ) - return False - + return _send_email(email, subject, html_content) except Exception as e: - logger.error(f"Error sending account locked email to {email}: {str(e)}") + logger.error(f"Error preparing account locked email to {email}: {str(e)}") return False + +def _send_email_smtp(to_email: str, subject: str, html_content: str) -> bool: + """ + Send an email using SMTP + + 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: + # Get SMTP settings from environment variables + smtp_host = os.getenv("SMTP_HOST") + smtp_port = int(os.getenv("SMTP_PORT", "587")) + smtp_user = os.getenv("SMTP_USER") + smtp_password = os.getenv("SMTP_PASSWORD") + from_email = os.getenv("SMTP_FROM") + use_tls = os.getenv("SMTP_USE_TLS", "false").lower() == "true" + use_ssl = os.getenv("SMTP_USE_SSL", "false").lower() == "true" + + # Create message + message = MIMEMultipart() + message["From"] = from_email + message["To"] = to_email + message["Subject"] = subject + message.attach(MIMEText(html_content, "html")) + + # Connect to SMTP server + if use_ssl: + server = smtplib.SMTP_SSL(smtp_host, smtp_port) + else: + server = smtplib.SMTP(smtp_host, smtp_port) + if use_tls: + server.starttls() + + # Login if credentials provided + if smtp_user and smtp_password: + server.login(smtp_user, smtp_password) + + # Send email + server.send_message(message) + server.quit() + + logger.info(f"Email sent to {to_email} via SMTP") + 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 (SendGrid or SMTP) + + 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 + """ + email_provider = os.getenv("EMAIL_PROVIDER", "sendgrid").lower() + + if email_provider == "smtp": + return _send_email_smtp(to_email, subject, html_content) + else: + # Default to SendGrid + try: + sg = sendgrid.SendGridAPIClient(api_key=os.getenv("SENDGRID_API_KEY")) + from_email = Email(os.getenv("EMAIL_FROM")) + to_email_obj = To(to_email) + content = Content("text/html", html_content) + + mail = Mail(from_email, to_email_obj, 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 to {to_email} via SendGrid") + 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 \ No newline at end of file