feat(email): implement SMTP email sending functionality and update environment configuration
This commit is contained in:
parent
6cfff4cc95
commit
769f6bf97b
10
.env.example
10
.env.example
@ -38,6 +38,16 @@ ENCRYPTION_KEY="your-encryption-key"
|
|||||||
SENDGRID_API_KEY="your-sendgrid-api-key"
|
SENDGRID_API_KEY="your-sendgrid-api-key"
|
||||||
EMAIL_FROM="noreply@yourdomain.com"
|
EMAIL_FROM="noreply@yourdomain.com"
|
||||||
APP_URL="https://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_PUBLIC_KEY="your-langfuse-public-key"
|
||||||
LANGFUSE_SECRET_KEY="your-langfuse-secret-key"
|
LANGFUSE_SECRET_KEY="your-langfuse-secret-key"
|
||||||
|
@ -34,6 +34,10 @@ from datetime import datetime
|
|||||||
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
# SMTP
|
||||||
|
import smtplib
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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)}")
|
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>"
|
return f"<p>Could not display email content. Please access {context.get('verification_link', '') or context.get('reset_link', '')}</p>"
|
||||||
|
|
||||||
|
|
||||||
def send_verification_email(email: str, token: str) -> bool:
|
def send_verification_email(email: str, token: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Send a verification email to the user
|
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
|
bool: True if the email was sent successfully, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
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"
|
subject = "Email Verification - Evo AI"
|
||||||
|
|
||||||
verification_link = f"{os.getenv('APP_URL')}/security/verify-email?code={token}"
|
verification_link = f"{os.getenv('APP_URL')}/security/verify-email?code={token}"
|
||||||
|
|
||||||
html_content = _render_template(
|
html_content = _render_template(
|
||||||
"verification_email",
|
"verification_email",
|
||||||
{
|
{
|
||||||
"verification_link": verification_link,
|
"verification_link": verification_link,
|
||||||
"user_name": email.split("@")[
|
"user_name": email.split("@")[0],
|
||||||
0
|
|
||||||
], # Use part of the email as temporary name
|
|
||||||
"current_year": datetime.now().year,
|
"current_year": datetime.now().year,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
content = Content("text/html", html_content)
|
return _send_email(email, subject, 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
|
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|
||||||
|
|
||||||
def send_password_reset_email(email: str, token: str) -> bool:
|
def send_password_reset_email(email: str, token: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Send a password reset email to the user
|
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
|
bool: True if the email was sent successfully, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
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"
|
subject = "Password Reset - Evo AI"
|
||||||
|
|
||||||
reset_link = f"{os.getenv('APP_URL')}/security/reset-password?token={token}"
|
reset_link = f"{os.getenv('APP_URL')}/security/reset-password?token={token}"
|
||||||
|
|
||||||
html_content = _render_template(
|
html_content = _render_template(
|
||||||
"password_reset",
|
"password_reset",
|
||||||
{
|
{
|
||||||
"reset_link": reset_link,
|
"reset_link": reset_link,
|
||||||
"user_name": email.split("@")[
|
"user_name": email.split("@")[0],
|
||||||
0
|
|
||||||
], # Use part of the email as temporary name
|
|
||||||
"current_year": datetime.now().year,
|
"current_year": datetime.now().year,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
content = Content("text/html", html_content)
|
return _send_email(email, subject, 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
|
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|
||||||
|
|
||||||
def send_welcome_email(email: str, user_name: str = None) -> bool:
|
def send_welcome_email(email: str, user_name: str = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Send a welcome email to the user after verification
|
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
|
bool: True if the email was sent successfully, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
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"
|
subject = "Welcome to Evo AI"
|
||||||
|
|
||||||
dashboard_link = f"{os.getenv('APP_URL')}/dashboard"
|
dashboard_link = f"{os.getenv('APP_URL')}/dashboard"
|
||||||
|
|
||||||
html_content = _render_template(
|
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)
|
return _send_email(email, subject, 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
|
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|
||||||
|
|
||||||
def send_account_locked_email(
|
def send_account_locked_email(
|
||||||
email: str, reset_token: str, failed_attempts: int, time_period: str
|
email: str, reset_token: str, failed_attempts: int, time_period: str
|
||||||
) -> bool:
|
) -> bool:
|
||||||
@ -228,14 +173,8 @@ def send_account_locked_email(
|
|||||||
bool: True if the email was sent successfully, False otherwise
|
bool: True if the email was sent successfully, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
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"
|
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(
|
html_content = _render_template(
|
||||||
"account_locked",
|
"account_locked",
|
||||||
@ -248,20 +187,97 @@ def send_account_locked_email(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
content = Content("text/html", html_content)
|
return _send_email(email, subject, 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
|
|
||||||
|
|
||||||
except Exception as e:
|
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
|
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
|
Loading…
Reference in New Issue
Block a user