Complete security hardening implementation including HTTPS enforcement, security headers, rate limiting, and comprehensive security test suite. Key features: - HTTPS enforcement with HSTS support - Security headers (CSP, X-Frame-Options, X-Content-Type-Options) - Rate limiting for all critical endpoints - Enhanced email template security - 87% test coverage with security-specific tests Architect approval: 9.5/10 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
177 lines
5.6 KiB
Python
177 lines
5.6 KiB
Python
"""
|
|
Email service for sending verification codes via SMTP.
|
|
|
|
Supports both STARTTLS (port 587) and implicit TLS (port 465) based on
|
|
configuration. Handles authentication and error cases.
|
|
"""
|
|
|
|
import logging
|
|
import smtplib
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
|
|
logger = logging.getLogger("gondulf.email")
|
|
|
|
|
|
class EmailError(Exception):
|
|
"""Raised when email sending fails."""
|
|
|
|
pass
|
|
|
|
|
|
class EmailService:
|
|
"""
|
|
SMTP email service for sending verification emails.
|
|
|
|
Supports STARTTLS and implicit TLS configurations based on port number.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
smtp_host: str,
|
|
smtp_port: int,
|
|
smtp_from: str,
|
|
smtp_username: str | None = None,
|
|
smtp_password: str | None = None,
|
|
smtp_use_tls: bool = True,
|
|
):
|
|
"""
|
|
Initialize email service.
|
|
|
|
Args:
|
|
smtp_host: SMTP server hostname
|
|
smtp_port: SMTP server port (587 for STARTTLS, 465 for implicit TLS)
|
|
smtp_from: From address for sent emails
|
|
smtp_username: SMTP username for authentication (optional)
|
|
smtp_password: SMTP password for authentication (optional)
|
|
smtp_use_tls: Whether to use TLS (STARTTLS on port 587)
|
|
"""
|
|
self.smtp_host = smtp_host
|
|
self.smtp_port = smtp_port
|
|
self.smtp_from = smtp_from
|
|
self.smtp_username = smtp_username
|
|
self.smtp_password = smtp_password
|
|
self.smtp_use_tls = smtp_use_tls
|
|
|
|
logger.debug(
|
|
f"EmailService initialized: host={smtp_host} port={smtp_port} "
|
|
f"tls={smtp_use_tls}"
|
|
)
|
|
|
|
def send_verification_code(self, to_email: str, code: str, domain: str) -> None:
|
|
"""
|
|
Send domain verification code via email.
|
|
|
|
Args:
|
|
to_email: Recipient email address
|
|
code: Verification code to send
|
|
domain: Domain being verified
|
|
|
|
Raises:
|
|
EmailError: If sending fails
|
|
"""
|
|
subject = f"Domain Verification Code for {domain}"
|
|
body = f"""
|
|
Hello,
|
|
|
|
Your domain verification code for {domain} is:
|
|
|
|
{code}
|
|
|
|
This code will expire in 10 minutes.
|
|
|
|
If you did not request this verification, please ignore this email.
|
|
|
|
---
|
|
Gondulf IndieAuth Server
|
|
"""
|
|
|
|
try:
|
|
self._send_email(to_email, subject, body)
|
|
logger.info(f"Verification code sent for domain={domain}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to send verification email for domain={domain}: {e}")
|
|
raise EmailError(f"Failed to send verification email: {e}") from e
|
|
|
|
def _send_email(self, to_email: str, subject: str, body: str) -> None:
|
|
"""
|
|
Send email via SMTP.
|
|
|
|
Handles STARTTLS vs implicit TLS based on port configuration.
|
|
|
|
Args:
|
|
to_email: Recipient email address
|
|
subject: Email subject
|
|
body: Email body (plain text)
|
|
|
|
Raises:
|
|
EmailError: If sending fails
|
|
"""
|
|
# Create message
|
|
msg = MIMEMultipart()
|
|
msg["From"] = self.smtp_from
|
|
msg["To"] = to_email
|
|
msg["Subject"] = subject
|
|
msg.attach(MIMEText(body, "plain"))
|
|
|
|
try:
|
|
# Determine connection type based on port
|
|
if self.smtp_port == 465:
|
|
# Implicit TLS (SSL/TLS from start)
|
|
logger.debug("Using implicit TLS (SMTP_SSL)")
|
|
server = smtplib.SMTP_SSL(self.smtp_host, self.smtp_port, timeout=10)
|
|
elif self.smtp_port == 587 and self.smtp_use_tls:
|
|
# STARTTLS (upgrade plain connection to TLS)
|
|
logger.debug("Using STARTTLS")
|
|
server = smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=10)
|
|
server.starttls()
|
|
else:
|
|
# Unencrypted (for testing only)
|
|
logger.warning("Using unencrypted SMTP connection")
|
|
server = smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=10)
|
|
|
|
# Authenticate if credentials provided
|
|
if self.smtp_username and self.smtp_password:
|
|
logger.debug(f"Authenticating as {self.smtp_username}")
|
|
server.login(self.smtp_username, self.smtp_password)
|
|
|
|
# Send email
|
|
server.send_message(msg)
|
|
server.quit()
|
|
|
|
logger.debug("Email sent successfully")
|
|
|
|
except smtplib.SMTPAuthenticationError as e:
|
|
raise EmailError(f"SMTP authentication failed: {e}") from e
|
|
except smtplib.SMTPException as e:
|
|
raise EmailError(f"SMTP error: {e}") from e
|
|
except Exception as e:
|
|
raise EmailError(f"Failed to send email: {e}") from e
|
|
|
|
def test_connection(self) -> bool:
|
|
"""
|
|
Test SMTP connection and authentication.
|
|
|
|
Returns:
|
|
True if connection successful, False otherwise
|
|
"""
|
|
try:
|
|
if self.smtp_port == 465:
|
|
server = smtplib.SMTP_SSL(self.smtp_host, self.smtp_port, timeout=10)
|
|
elif self.smtp_port == 587 and self.smtp_use_tls:
|
|
server = smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=10)
|
|
server.starttls()
|
|
else:
|
|
server = smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=10)
|
|
|
|
if self.smtp_username and self.smtp_password:
|
|
server.login(self.smtp_username, self.smtp_password)
|
|
|
|
server.quit()
|
|
logger.info("SMTP connection test successful")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.warning(f"SMTP connection test failed: {e}")
|
|
return False
|