""" 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