"""
SendGrid email service for password authentication.
Sends setup and password reset emails to external users.
"""
import logging
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Email, Mail
from .config import Config
logger = logging.getLogger(__name__)
def _get_sendgrid_client() -> SendGridAPIClient | None:
"""Get SendGrid client if API key is configured."""
if not Config.SENDGRID_API_KEY:
logger.warning("SENDGRID_API_KEY not configured, emails will not be sent")
return None
return SendGridAPIClient(Config.SENDGRID_API_KEY)
def send_setup_email(email: str, token: str, base_url: str) -> tuple[bool, str]:
"""Send account setup email with magic link.
Args:
email: Recipient email address
token: Setup token for the magic link
base_url: Base URL of the application (e.g., https://data.example.com)
Returns:
Tuple of (success, message)
"""
client = _get_sendgrid_client()
if not client:
return False, "Email service not configured"
setup_url = f"{base_url}/auth/setup/{token}"
instance_name = Config.INSTANCE_NAME
subject = f"Set up your {instance_name} account"
html_content = f"""
Hello,
You've been granted access to the {instance_name} platform.
Click the button below to set up your password and complete your account setup.
Set Up Your Account
This link expires in 24 hours.
If you didn't request access to this platform, you can safely ignore this email.
Or copy and paste this URL into your browser:
{setup_url}
"""
plain_content = f"""
{instance_name} - Account Setup
Hello,
You've been granted access to the {instance_name} platform.
Visit the link below to set up your password and complete your account setup:
{setup_url}
This link expires in 24 hours.
If you didn't request access to this platform, you can safely ignore this email.
---
This is an automated message from {instance_name} platform.
"""
from_email = Email(Config.EMAIL_FROM_ADDRESS, Config.EMAIL_FROM_NAME)
message = Mail(
from_email=from_email,
to_emails=email,
subject=subject,
html_content=html_content,
plain_text_content=plain_content,
)
try:
response = client.send(message)
if response.status_code in (200, 201, 202):
logger.info(f"Setup email sent to {email}")
return True, "Setup email sent successfully"
logger.error(f"SendGrid error: status {response.status_code}")
return False, f"Failed to send email (status {response.status_code})"
except Exception as e:
logger.exception(f"Failed to send setup email to {email}: {e}")
return False, f"Failed to send email: {e}"
def send_reset_email(email: str, token: str, base_url: str) -> tuple[bool, str]:
"""Send password reset email with magic link.
Args:
email: Recipient email address
token: Reset token for the magic link
base_url: Base URL of the application
Returns:
Tuple of (success, message)
"""
client = _get_sendgrid_client()
if not client:
return False, "Email service not configured"
reset_url = f"{base_url}/auth/reset/{token}"
instance_name = Config.INSTANCE_NAME
subject = f"Reset your {instance_name} password"
html_content = f"""
Hello,
We received a request to reset your password for your {instance_name} account.
Click the button below to set a new password.
Reset Password
This link expires in 1 hour.
If you didn't request a password reset, you can safely ignore this email.
Your password will remain unchanged.
Or copy and paste this URL into your browser:
{reset_url}
"""
plain_content = f"""
{instance_name} - Password Reset
Hello,
We received a request to reset your password for your {instance_name} account.
Visit the link below to set a new password:
{reset_url}
This link expires in 1 hour.
If you didn't request a password reset, you can safely ignore this email.
Your password will remain unchanged.
---
This is an automated message from {instance_name} platform.
"""
from_email = Email(Config.EMAIL_FROM_ADDRESS, Config.EMAIL_FROM_NAME)
message = Mail(
from_email=from_email,
to_emails=email,
subject=subject,
html_content=html_content,
plain_text_content=plain_content,
)
try:
response = client.send(message)
if response.status_code in (200, 201, 202):
logger.info(f"Reset email sent to {email}")
return True, "Reset email sent successfully"
logger.error(f"SendGrid error: status {response.status_code}")
return False, f"Failed to send email (status {response.status_code})"
except Exception as e:
logger.exception(f"Failed to send reset email to {email}: {e}")
return False, f"Failed to send email: {e}"