This is an automated email from the ASF dual-hosted git repository.
shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new a7b8dc96928 Give the SMTP emailer its own defaults for the [smtp]
options (#76346)
a7b8dc96928 is described below
commit a7b8dc9692804b29c128140290e8feba7b2f4e57
Author: Shivam Rastogi <[email protected]>
AuthorDate: Fri Nov 6 07:39:33 2026 -0800
Give the SMTP emailer its own defaults for the [smtp] options (#76346)
* Give the SMTP emailer its own defaults for the [smtp] options
The emailer reads each [smtp] option with a fallback equal to the default
core
defines today, so it keeps working when core no longer defines the section.
Tests assert the default values instead of reading them from core's config,
and the provider docs list the options and defaults.
* Drop the [smtp] options table from the SMTP email backend docs
Core's configuration reference still lists these options.
* Address review comments on the SMTP emailer defaults
Scope the docs note to this provider, drop the unreachable from_email
branch, reword the fallback comment, and use conf_vars as a decorator in the
new test.
---
providers/smtp/docs/connections/smtp.rst | 9 +-
.../src/airflow/providers/smtp/utils/emailer.py | 24 ++---
.../smtp/tests/unit/smtp/utils/test_emailer.py | 108 ++++++++++++++-------
3 files changed, 87 insertions(+), 54 deletions(-)
diff --git a/providers/smtp/docs/connections/smtp.rst
b/providers/smtp/docs/connections/smtp.rst
index 2d0ff2add52..53cdf6b0a06 100644
--- a/providers/smtp/docs/connections/smtp.rst
+++ b/providers/smtp/docs/connections/smtp.rst
@@ -233,11 +233,10 @@ connection via **CLI**:
}'
.. note::
- The ``[smtp]`` section in ``airflow.cfg`` is used by the **core**
- e-mail helper slated for deprecation.
- When you switch to :class:`~airflow.providers.smtp.hooks.smtp.SmtpHook`
- *and* supply a ``smtp_conn_id``, the hook's connection settings take
- precedence and the global ``[smtp]`` options may be ignored.
+ Within this provider, the ``[smtp]`` section is used only by the email
backend in
+ :mod:`airflow.providers.smtp.utils.emailer` (see :doc:`/email-backend`).
+ :class:`~airflow.providers.smtp.hooks.smtp.SmtpHook` and the SMTP notifier
never read it;
+ they take all settings from the Connection.
Using ``SmtpHook`` in a Dag
^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/providers/smtp/src/airflow/providers/smtp/utils/emailer.py
b/providers/smtp/src/airflow/providers/smtp/utils/emailer.py
index c735735d800..807f30880c2 100644
--- a/providers/smtp/src/airflow/providers/smtp/utils/emailer.py
+++ b/providers/smtp/src/airflow/providers/smtp/utils/emailer.py
@@ -128,16 +128,7 @@ def send_email_smtp(
>>> send_email("[email protected]", "foo", "<b>Foo</b> bar", ["/dev/null"],
dryrun=True)
"""
- smtp_mail_from = conf.get("smtp", "SMTP_MAIL_FROM")
-
- if smtp_mail_from is not None:
- mail_from = smtp_mail_from
- else:
- if from_email is None:
- raise ValueError(
- "You should set from email - either by smtp/smtp_mail_from
config or `from_email` parameter"
- )
- mail_from = from_email
+ mail_from = conf.get("smtp", "SMTP_MAIL_FROM",
fallback="[email protected]")
msg, recipients = build_mime_message(
mail_from=mail_from,
@@ -236,12 +227,13 @@ def send_mime_email(
: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")
- smtp_ssl = conf.getboolean("smtp", "SMTP_SSL")
- smtp_retry_limit = conf.getint("smtp", "SMTP_RETRY_LIMIT")
- smtp_timeout = conf.getint("smtp", "SMTP_TIMEOUT")
+ # Same defaults as core's [smtp] section, so email keeps working once core
stops defining it.
+ smtp_host = conf.get_mandatory_value("smtp", "SMTP_HOST",
fallback="localhost")
+ smtp_port = conf.getint("smtp", "SMTP_PORT", fallback=25)
+ smtp_starttls = conf.getboolean("smtp", "SMTP_STARTTLS", fallback=True)
+ smtp_ssl = conf.getboolean("smtp", "SMTP_SSL", fallback=False)
+ smtp_retry_limit = conf.getint("smtp", "SMTP_RETRY_LIMIT", fallback=5)
+ smtp_timeout = conf.getint("smtp", "SMTP_TIMEOUT", fallback=30)
smtp_user = None
smtp_password = None
diff --git a/providers/smtp/tests/unit/smtp/utils/test_emailer.py
b/providers/smtp/tests/unit/smtp/utils/test_emailer.py
index f2386216bb7..33fb002c155 100644
--- a/providers/smtp/tests/unit/smtp/utils/test_emailer.py
+++ b/providers/smtp/tests/unit/smtp/utils/test_emailer.py
@@ -17,6 +17,7 @@
# under the License.
from __future__ import annotations
+import copy
import json
import os
from email.mime.application import MIMEApplication
@@ -227,11 +228,11 @@ class TestEmailerSmtp:
emailer.send_email_smtp("to", "subject", "content",
files=[os.fspath(path)])
assert mock_send_mime.called
_, call_args = mock_send_mime.call_args
- assert conf.get("smtp", "SMTP_MAIL_FROM") == call_args["e_from"]
+ assert call_args["e_from"] == "[email protected]"
assert call_args["e_to"] == ["to"]
msg = call_args["mime_msg"]
assert msg["Subject"] == "subject"
- assert conf.get("smtp", "SMTP_MAIL_FROM") == msg["From"]
+ assert msg["From"] == "[email protected]"
assert len(msg.get_payload()) == 2
filename = f'attachment; filename="{path.name}"'
assert filename == msg.get_payload()[-1].get("Content-Disposition")
@@ -262,11 +263,11 @@ class TestEmailerSmtp:
)
assert mock_send_mime.called
_, call_args = mock_send_mime.call_args
- assert conf.get("smtp", "SMTP_MAIL_FROM") == call_args["e_from"]
+ assert call_args["e_from"] == "[email protected]"
assert call_args["e_to"] == ["to", "cc", "bcc"]
msg = call_args["mime_msg"]
assert msg["Subject"] == "subject"
- assert conf.get("smtp", "SMTP_MAIL_FROM") == msg["From"]
+ assert msg["From"] == "[email protected]"
assert len(msg.get_payload()) == 2
assert f'attachment; filename="{path.name}"' ==
msg.get_payload()[-1].get("Content-Disposition")
mimeapp = MIMEApplication("attachment")
@@ -281,9 +282,9 @@ class TestEmailerSmtp:
msg = MIMEMultipart()
emailer.send_mime_email("from", "to", msg, dryrun=False)
mock_smtp.assert_called_once_with(
- host=conf.get("smtp", "SMTP_HOST"),
- port=conf.getint("smtp", "SMTP_PORT"),
- timeout=conf.getint("smtp", "SMTP_TIMEOUT"),
+ host="localhost",
+ port=25,
+ timeout=30,
)
assert not mock_smtp_ssl.called
assert mock_smtp.return_value.starttls.called
@@ -317,9 +318,9 @@ class TestEmailerSmtp:
emailer.send_mime_email("from", "to", MIMEMultipart(),
dryrun=False)
assert not mock_smtp.called
mock_smtp_ssl.assert_called_once_with(
- host=conf.get("smtp", "SMTP_HOST"),
- port=conf.getint("smtp", "SMTP_PORT"),
- timeout=conf.getint("smtp", "SMTP_TIMEOUT"),
+ host="localhost",
+ port=25,
+ timeout=30,
context=None,
)
@@ -333,9 +334,9 @@ class TestEmailerSmtp:
assert not mock_smtp.called
assert create_default_context.called
mock_smtp_ssl.assert_called_once_with(
- host=conf.get("smtp", "SMTP_HOST"),
- port=conf.getint("smtp", "SMTP_PORT"),
- timeout=conf.getint("smtp", "SMTP_TIMEOUT"),
+ host="localhost",
+ port=25,
+ timeout=30,
context=create_default_context.return_value,
)
@@ -351,9 +352,9 @@ class TestEmailerSmtp:
assert not mock_smtp.called
assert create_default_context.called
mock_smtp_ssl.assert_called_once_with(
- host=conf.get("smtp", "SMTP_HOST"),
- port=conf.getint("smtp", "SMTP_PORT"),
- timeout=conf.getint("smtp", "SMTP_TIMEOUT"),
+ host="localhost",
+ port=25,
+ timeout=30,
context=create_default_context.return_value,
)
@@ -370,9 +371,9 @@ class TestEmailerSmtp:
emailer.send_mime_email("from", "to", MIMEMultipart(),
dryrun=False)
assert not mock_smtp_ssl.called
mock_smtp.assert_called_once_with(
- host=conf.get("smtp", "SMTP_HOST"),
- port=conf.getint("smtp", "SMTP_PORT"),
- timeout=conf.getint("smtp", "SMTP_TIMEOUT"),
+ host="localhost",
+ port=25,
+ timeout=30,
)
assert not mock_smtp.login.called
@@ -391,11 +392,11 @@ class TestEmailerSmtp:
with pytest.raises(SMTPServerDisconnected):
emailer.send_mime_email("from", "to", msg, dryrun=False)
mock_smtp.assert_any_call(
- host=conf.get("smtp", "SMTP_HOST"),
- port=conf.getint("smtp", "SMTP_PORT"),
- timeout=conf.getint("smtp", "SMTP_TIMEOUT"),
+ host="localhost",
+ port=25,
+ timeout=30,
)
- assert mock_smtp.call_count == conf.getint("smtp", "SMTP_RETRY_LIMIT")
+ 1
+ assert mock_smtp.call_count == 5 + 1
assert not mock_smtp_ssl.called
assert not mock_smtp.return_value.starttls.called
assert not mock_smtp.return_value.login.called
@@ -413,13 +414,13 @@ class TestEmailerSmtp:
emailer.send_mime_email("from", "to", msg, dryrun=False)
mock_smtp_ssl.assert_any_call(
- host=conf.get("smtp", "SMTP_HOST"),
- port=conf.getint("smtp", "SMTP_PORT"),
- timeout=conf.getint("smtp", "SMTP_TIMEOUT"),
+ host="localhost",
+ port=25,
+ timeout=30,
context=create_default_context.return_value,
)
assert create_default_context.called
- assert mock_smtp_ssl.call_count == conf.getint("smtp",
"SMTP_RETRY_LIMIT") + 1
+ assert mock_smtp_ssl.call_count == 5 + 1
assert not mock_smtp.called
assert not mock_smtp_ssl.return_value.starttls.called
assert not mock_smtp_ssl.return_value.login.called
@@ -444,9 +445,7 @@ class TestEmailerSmtp:
with pytest.raises(SMTPServerDisconnected):
emailer.send_mime_email("from", "to", msg, dryrun=False)
- mock_smtp.assert_any_call(
- host=conf.get("smtp", "SMTP_HOST"), port=conf.getint("smtp",
"SMTP_PORT"), timeout=custom_timeout
- )
+ mock_smtp.assert_any_call(host="localhost", port=25,
timeout=custom_timeout)
assert not mock_smtp_ssl.called
assert mock_smtp.call_count == custom_retry_limit + 1
@@ -461,9 +460,9 @@ class TestEmailerSmtp:
emailer.send_mime_email("from", "to", msg, dryrun=False)
mock_smtp.assert_any_call(
- host=conf.get("smtp", "SMTP_HOST"),
- port=conf.getint("smtp", "SMTP_PORT"),
- timeout=conf.getint("smtp", "SMTP_TIMEOUT"),
+ host="localhost",
+ port=25,
+ timeout=30,
)
assert mock_smtp.call_count == side_effects.index(final_mock) + 1
assert not mock_smtp_ssl.called
@@ -523,3 +522,46 @@ class TestEmailerSmtp:
emailer.send_mime_email("from", "to", MIMEMultipart())
assert not mock_smtp.return_value.starttls.called
assert mock_smtp.return_value.sendmail.called
+
+ @pytest.fixture
+ def core_without_smtp_section(self, monkeypatch):
+ """Drop core's ``[smtp]`` defaults, like a core release that no longer
defines the section."""
+ defaults = copy.deepcopy(conf._default_values)
+ defaults.remove_section("smtp")
+ monkeypatch.setattr(conf, "_default_values", defaults)
+ monkeypatch.delenv("AIRFLOW_CONN_SMTP_DEFAULT", raising=False)
+ with conf_vars({("smtp", "smtp_mail_from"): None}):
+ yield
+
+ @mock.patch("smtplib.SMTP_SSL")
+ @mock.patch("smtplib.SMTP")
+ def test_send_without_core_smtp_section_uses_built_in_defaults(
+ self, mock_smtp, mock_smtp_ssl, core_without_smtp_section
+ ):
+ emailer.send_email_smtp("to", "subject", "content")
+ mock_smtp.assert_called_once_with(host="localhost", port=25,
timeout=30)
+ assert not mock_smtp_ssl.called
+ assert mock_smtp.return_value.starttls.called
+ assert mock_smtp.return_value.sendmail.call_args.args[0] ==
"[email protected]"
+
+ @mock.patch("smtplib.SMTP")
+ def test_retry_without_core_smtp_section_uses_built_in_limit(self,
mock_smtp, core_without_smtp_section):
+ mock_smtp.side_effect = SMTPServerDisconnected()
+ with pytest.raises(SMTPServerDisconnected):
+ emailer.send_mime_email("from", "to", MIMEMultipart())
+ assert mock_smtp.call_count == 5 + 1
+
+ @mock.patch("smtplib.SMTP")
+ @conf_vars(
+ {
+ ("smtp", "smtp_host"): "smtp.example.com",
+ ("smtp", "smtp_port"): "2525",
+ ("smtp", "smtp_mail_from"): "[email protected]",
+ }
+ )
+ def test_send_without_core_smtp_section_uses_configured_values(
+ self, mock_smtp, core_without_smtp_section
+ ):
+ emailer.send_email_smtp("to", "subject", "content")
+ mock_smtp.assert_called_once_with(host="smtp.example.com", port=2525,
timeout=30)
+ assert mock_smtp.return_value.sendmail.call_args.args[0] ==
"[email protected]"