Source code for smarter.common.helpers.email_helpers

"""Helper class for sending email via AWS Simple Email Service using SMTP."""

import logging
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import List, Union

from smarter.common.conf import smarter_settings
from smarter.common.exceptions import SmarterException
from smarter.common.helpers.console_helpers import formatted_text
from smarter.lib.django.validators import SmarterValidator

from ..mixins import Singleton

logger = logging.getLogger(__name__)


class EmailHelperException(SmarterException):
    """Base class for Email helper exceptions."""


HERE = __name__


[docs] class EmailHelper(metaclass=Singleton): """ Helper class for sending emails via AWS Simple Email Service (SES) using SMTP. This class provides utility methods for validating email addresses and sending emails through an SMTP server, with configuration provided by Django settings and custom application settings. The class is implemented as a singleton to ensure a single instance is used throughout the application. """ logger_prefix = formatted_text(f"{HERE}.{__qualname__}()")
[docs] @staticmethod def validate_mail_list(emails: Union[str, List[str]], quiet: bool = False) -> Union[List[str], None]: """ Convert the input into a list and filter out any invalid email addresses. This method accepts either a single email address as a string or a list of email addresses. It validates each email address using the `SmarterValidator.is_valid_email` method. Invalid email addresses are filtered out, and warnings are logged if any are found, unless `quiet` is set to True. Parameters ---------- emails : Union[str, List[str]] A single email address as a string, or a list of email addresses to validate. quiet : bool, optional If True, suppresses warning logs for invalid email addresses (default is False). Returns ------- Union[List[str], None] A list of valid email addresses, or None if no valid addresses are found. Logs ---- - Logs a warning if the input is not a string or list. - Logs a warning if invalid email addresses are found (unless `quiet` is True). - Logs a warning if no valid email addresses are found (unless `quiet` is True). """ if isinstance(emails, str): mailto_list = [emails] elif isinstance(emails, list): mailto_list = emails else: logger.warning("%s invalid email address list provided: %s", EmailHelper.logger_prefix, emails) return None valid_emails = [email for email in mailto_list if SmarterValidator.is_valid_email(email)] if len(valid_emails) != len(mailto_list) and set(mailto_list) != set(valid_emails) and not quiet: diff = set(mailto_list) != set(valid_emails) if diff != [""]: logger.warning( "%s invalid email addresses were found in send list: %s", EmailHelper.logger_prefix, set(mailto_list) - set(valid_emails), ) if len(valid_emails) == 0 and not quiet: logger.warning("%s no valid email addresses found in send list", EmailHelper.logger_prefix) return None return valid_emails
# pylint: disable=too-many-arguments
[docs] @staticmethod def send_email(subject, body, to: Union[str, List[str]], html=False, from_email=None, quiet: bool = False): """ Send an email using the configured SMTP server. This method constructs and sends an email message using the SMTP configuration specified in the application settings. It supports sending plain text or HTML emails, and can optionally suppress actual sending for testing or development purposes. Parameters ---------- subject : str The subject line of the email. body : str The body content of the email. If `html` is True, this should be HTML-formatted. to : Union[str, List[str]] The recipient email address or a list of recipient email addresses. html : bool, optional If True, sends the email as HTML. Otherwise, sends as plain text (default is False). from_email : str, optional The sender's email address. If not provided, uses the configured default sender. quiet : bool, optional If True, does not actually send the email and suppresses warnings (default is False). Raises ------ EmailHelperException If required SMTP configuration is missing or if an error occurs during sending and developer mode is enabled. Logs ---- - Logs a warning if SMTP is not configured. - Logs information about emails that would have been sent in quiet mode. - Logs errors if sending fails due to SMTP or unexpected exceptions. """ if not smarter_settings.smtp_is_configured: if not quiet: logger.warning( "%s quiet mode. SMTP not configured, would have sent subject '%s' to: %s", EmailHelper.logger_prefix, subject, to, ) return mail_to = EmailHelper.validate_mail_list(emails=to, quiet=quiet) if mail_to in (None, []): return if quiet: logger.debug( "%s EmailHelper.send_email() quiet mode. would have sent subject '%s' to: %s", EmailHelper.logger_prefix, subject, mail_to, ) return if not mail_to: return if not smarter_settings.smtp_from_email: raise EmailHelperException("smtp_from_email not configured") if smarter_settings.smtp_host is None or smarter_settings.smtp_port is None: raise EmailHelperException("SMTP host or port not configured") if smarter_settings.smtp_username is None or smarter_settings.smtp_password is None: raise EmailHelperException("SMTP username or password not configured") msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = from_email or smarter_settings.smtp_from_email msg["To"] = ", ".join(mail_to) msg["Bcc"] = smarter_settings.email_admin part2 = MIMEText(body, "html") if html else MIMEText(body) msg.attach(part2) try: with smtplib.SMTP(smarter_settings.smtp_host, smarter_settings.smtp_port) as server: if smarter_settings.smtp_use_tls: server.starttls() server.login( smarter_settings.smtp_username.get_secret_value(), smarter_settings.smtp_password.get_secret_value() ) server.sendmail(msg["From"], [msg["To"]], msg.as_string()) logger.info("%s smtp email sent to %s: %s", EmailHelper.logger_prefix, to, subject) except ( smtplib.SMTPDataError, smtplib.SMTPAuthenticationError, smtplib.SMTPConnectError, smtplib.SMTPHeloError, smtplib.SMTPRecipientsRefused, smtplib.SMTPSenderRefused, smtplib.SMTPServerDisconnected, smtplib.SMTPNotSupportedError, ) as e: logger.error( "%s smtp error while attempting to send email. error: %s from: %s to. %s", EmailHelper.logger_prefix, e, msg["From"], msg["To"], ) # pylint: disable=broad-except except Exception as e: logger.error( "%s unexpected error while attempting to send email. error: %s from: %s to. %s", EmailHelper.logger_prefix, e, msg["From"], msg["To"], ) except SystemExit as e: logger.error( "%s system exit error while attempting to send email. error: %s from: %s to. %s", EmailHelper.logger_prefix, e, msg["From"], msg["To"], ) raise
email_helper = EmailHelper()