Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add doc-strings and small improvement to email util #28634

Merged
merged 7 commits into from
Dec 29, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 75 additions & 31 deletions airflow/utils/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import collections.abc
import logging
import os
import re
import smtplib
import warnings
from email.mime.application import MIMEApplication
Expand Down Expand Up @@ -47,8 +48,26 @@ def send_email(
conn_id: str | None = None,
custom_headers: dict[str, Any] | None = None,
**kwargs,
):
"""Send email using backend specified in EMAIL_BACKEND."""
) -> None:
"""
Send an email using the backend specified in the `EMAIL_BACKEND` configuration option.
stamixthereal marked this conversation as resolved.
Show resolved Hide resolved

:param to: A list or iterable of email addresses to send the email to.
:param subject: The subject of the email.
:param html_content: The content of the email in HTML format.
:param files: A list of paths to files to attach to the email.
:param dryrun: If `True`, the email will not actually be sent. Default: `False`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:param dryrun: If `True`, the email will not actually be sent. Default: `False`.
:param dryrun: If `True`, the email will not actually be sent. Default: `False`.

Either using double backticks or star (italic, matching the style used by the Python stdlib documentation)

:param cc: A string or iterable of strings containing email addresses to send a copy of the email to.
:param bcc: A string or iterable of strings containing email addresses to send a
blind carbon copy of the email to.
:param mime_subtype: The subtype of the MIME message. Default: "mixed".
:param mime_charset: The charset of the email. Default: "utf-8".
:param conn_id: The connection ID to use for the backend. If not provided, the default connection
specified in the `EMAIL_CONN_ID` configuration option will be used.
stamixthereal marked this conversation as resolved.
Show resolved Hide resolved
:param custom_headers: A dictionary of additional headers to add to the MIME message.
No validations are run on these values, and they should be able to be encoded.
:param kwargs: Additional keyword arguments to pass to the backend.
"""
backend = conf.getimport("email", "EMAIL_BACKEND")
backend_conn_id = conn_id or conf.get("email", "EMAIL_CONN_ID")
from_email = conf.get("email", "from_email", fallback=None)
Expand Down Expand Up @@ -87,7 +106,7 @@ def send_email_smtp(
from_email: str | None = None,
custom_headers: dict[str, Any] | None = None,
**kwargs,
):
) -> None:
potiuk marked this conversation as resolved.
Show resolved Hide resolved
"""
Send an email with html content

Expand Down Expand Up @@ -133,21 +152,20 @@ def build_mime_message(
custom_headers: dict[str, Any] | None = None,
) -> tuple[MIMEMultipart, list[str]]:
"""
Build a MIME message that can be used to send an email and
returns full list of recipients.

:param mail_from: Email address to set as email's from
:param to: List of email addresses to set as email's to
:param subject: Email's subject
:param html_content: Content of email in HTML format
:param files: List of paths of files to be attached
:param cc: List of email addresses to set as email's CC
:param bcc: List of email addresses to set as email's BCC
:param mime_subtype: Can be used to specify the subtype of the message. Default = mixed
:param mime_charset: Email's charset. Default = UTF-8.
:param custom_headers: Additional headers to add to the MIME message.
No validations are run on these values and they should be able to be encoded.
:return: Email as MIMEMultipart and list of recipients' addresses.
Build a MIME message that can be used to send an email and returns a full list of recipients.

:param mail_from: Email address to set as the email's "From" field.
:param to: A string or iterable of strings containing email addresses to set as the email's "To" field.
:param subject: The subject of the email.
:param html_content: The content of the email in HTML format.
:param files: A list of paths to files to be attached to the email.
:param cc: A string or iterable of strings containing email addresses to set as the email's "CC" field.
:param bcc: A string or iterable of strings containing email addresses to set as the email's "BCC" field.
:param mime_subtype: The subtype of the MIME message. Default: "mixed".
:param mime_charset: The charset of the email. Default: "utf-8".
:param custom_headers: Additional headers to add to the MIME message. No validations are run on these
values, and they should be able to be encoded.
:return: A tuple containing the email as a MIMEMultipart object and a list of recipient email addresses.
"""
to = get_email_address_list(to)

Expand All @@ -159,12 +177,12 @@ def build_mime_message(
if cc:
cc = get_email_address_list(cc)
msg["CC"] = ", ".join(cc)
recipients = recipients + cc
recipients += cc

if bcc:
# don't add bcc in header
bcc = get_email_address_list(bcc)
recipients = recipients + bcc
recipients += bcc

msg["Date"] = formatdate(localtime=True)
mime_text = MIMEText(html_content, "html", mime_charset)
Expand Down Expand Up @@ -192,7 +210,15 @@ def send_mime_email(
conn_id: str = "smtp_default",
dryrun: bool = False,
) -> None:
"""Send MIME email."""
"""
Send a MIME email.

:param e_from: The email address of the sender.
:param e_to: The email address or a list of email addresses of the recipient(s).
:param mime_msg: The MIME message to send.
:param conn_id: The ID of the SMTP connection to use.
:param dryrun: If `True`, the email will not be sent, but a log message will be generated.
"""
smtp_host = conf.get_mandatory_value("smtp", "SMTP_HOST")
smtp_port = conf.getint("smtp", "SMTP_PORT")
smtp_starttls = conf.getboolean("smtp", "SMTP_STARTTLS")
Expand Down Expand Up @@ -245,20 +271,33 @@ def send_mime_email(


def get_email_address_list(addresses: str | Iterable[str]) -> list[str]:
"""Get list of email addresses."""
"""
Returns a list of email addresses from the provided input.

:param addresses: A string or iterable of strings containing email addresses.
:return: A list of email addresses.
:raises TypeError: If the input is not a string or iterable of strings.
"""
if isinstance(addresses, str):
return _get_email_list_from_str(addresses)

elif isinstance(addresses, collections.abc.Iterable):
if not all(isinstance(item, str) for item in addresses):
raise TypeError("The items in your iterable must be strings.")
return list(addresses)

received_type = type(addresses).__name__
raise TypeError(f"Unexpected argument type: Received '{received_type}'.")
else:
raise TypeError(f"Unexpected argument type: Received '{type(addresses).__name__}'.")


def _get_smtp_connection(host: str, port: int, timeout: int, with_ssl: bool) -> smtplib.SMTP:
"""
Returns an SMTP connection to the specified host and port, with optional SSL encryption.

:param host: The hostname or IP address of the SMTP server.
:param port: The port number to connect to on the SMTP server.
:param timeout: The timeout in seconds for the connection.
:param with_ssl: Whether to use SSL encryption for the connection.
:return: An SMTP connection to the specified host and port.
"""
return (
smtplib.SMTP_SSL(host=host, port=port, timeout=timeout)
if with_ssl
Expand All @@ -267,8 +306,13 @@ def _get_smtp_connection(host: str, port: int, timeout: int, with_ssl: bool) ->


def _get_email_list_from_str(addresses: str) -> list[str]:
delimiters = [",", ";"]
for delimiter in delimiters:
if delimiter in addresses:
return [address.strip() for address in addresses.split(delimiter)]
return [addresses]
"""
Extract a list of email addresses from a string. The string
can contain multiple email addresses separated by
any of the following delimiters: ',' or ';'.

:param addresses: A string containing one or more email addresses.
:return: A list of email addresses.
"""
pattern = r"[,;]\s*"
return [address.strip() for address in re.split(pattern, addresses)]
potiuk marked this conversation as resolved.
Show resolved Hide resolved