This is an automated email from the ASF dual-hosted git repository.

husseinawala 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 42db67b57cc BREAKING_CHANGE: remove 
airflow.operators.email.EmailOperator in favor of SMTP provider operator 
(#46573)
42db67b57cc is described below

commit 42db67b57ccfe0348f87cf451be080561b12d188
Author: Hussein Awala <huss...@awala.fr>
AuthorDate: Sat Feb 8 21:21:49 2025 +0100

    BREAKING_CHANGE: remove airflow.operators.email.EmailOperator in favor of 
SMTP provider operator (#46573)
    
    * BREAKING_CHANGE: remove airflow.operators.email.EmailOperator in favor of 
SMTP provider operator
    
    * Update newsfragments/46572.significant.rst
    
    Co-authored-by: Wei Lee <weilee...@gmail.com>
    
    * fix static checks
    
    ---------
    
    Co-authored-by: Wei Lee <weilee...@gmail.com>
---
 airflow/example_dags/example_dag_decorator.py   | 21 +++---
 airflow/operators/email.py                      | 91 -------------------------
 docs/apache-airflow/core-concepts/operators.rst |  2 +-
 docs/apache-airflow/core-concepts/taskflow.rst  |  2 +-
 docs/apache-airflow/operators-and-hooks-ref.rst |  3 -
 newsfragments/46572.significant.rst             | 22 ++++++
 tests/operators/test_email.py                   | 62 -----------------
 7 files changed, 33 insertions(+), 170 deletions(-)

diff --git a/airflow/example_dags/example_dag_decorator.py 
b/airflow/example_dags/example_dag_decorator.py
index 0fed70fa2e9..995bdcb3687 100644
--- a/airflow/example_dags/example_dag_decorator.py
+++ b/airflow/example_dags/example_dag_decorator.py
@@ -24,7 +24,7 @@ import pendulum
 
 from airflow.decorators import dag, task
 from airflow.models.baseoperator import BaseOperator
-from airflow.operators.email import EmailOperator
+from airflow.providers.standard.operators.bash import BashOperator
 
 if TYPE_CHECKING:
     from airflow.sdk.definitions.context import Context
@@ -48,27 +48,24 @@ class GetRequestOperator(BaseOperator):
     catchup=False,
     tags=["example"],
 )
-def example_dag_decorator(email: str = "exam...@example.com"):
+def example_dag_decorator(url: str = "http://httpbin.org/get";):
     """
-    DAG to send server IP to email.
+    DAG to get IP address and echo it via BashOperator.
 
-    :param email: Email to send IP to. Defaults to exam...@example.com.
+    :param url: URL to get IP address from. Defaults to 
"http://httpbin.org/get";.
     """
-    get_ip = GetRequestOperator(task_id="get_ip", url="http://httpbin.org/get";)
+    get_ip = GetRequestOperator(task_id="get_ip", url=url)
 
     @task(multiple_outputs=True)
-    def prepare_email(raw_json: dict[str, Any]) -> dict[str, str]:
+    def prepare_command(raw_json: dict[str, Any]) -> dict[str, str]:
         external_ip = raw_json["origin"]
         return {
-            "subject": f"Server connected from {external_ip}",
-            "body": f"Seems like today your server executing Airflow is 
connected from IP {external_ip}<br>",
+            "command": f"echo 'Seems like today your server executing Airflow 
is connected from IP {external_ip}'",
         }
 
-    email_info = prepare_email(get_ip.output)
+    command_info = prepare_command(get_ip.output)
 
-    EmailOperator(
-        task_id="send_email", to=email, subject=email_info["subject"], 
html_content=email_info["body"]
-    )
+    BashOperator(task_id="echo_ip_info", bash_command=command_info["command"])
 
 
 example_dag = example_dag_decorator()
diff --git a/airflow/operators/email.py b/airflow/operators/email.py
deleted file mode 100644
index 85ac709e2bd..00000000000
--- a/airflow/operators/email.py
+++ /dev/null
@@ -1,91 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-from __future__ import annotations
-
-from collections.abc import Sequence
-from typing import TYPE_CHECKING, Any
-
-from airflow.models.baseoperator import BaseOperator
-from airflow.utils.email import send_email
-
-if TYPE_CHECKING:
-    from airflow.sdk.definitions.context import Context
-
-
-class EmailOperator(BaseOperator):
-    """
-    Sends an email.
-
-    :param to: list of emails to send the email to. (templated)
-    :param subject: subject line for the email. (templated)
-    :param html_content: content of the email, html markup
-        is allowed. (templated)
-    :param files: file names to attach in email (templated)
-    :param cc: list of recipients to be added in CC field
-    :param bcc: list of recipients to be added in BCC field
-    :param mime_subtype: MIME sub content type
-    :param mime_charset: character set parameter added to the Content-Type
-        header.
-    :param custom_headers: additional headers to add to the MIME message.
-    """
-
-    template_fields: Sequence[str] = ("to", "subject", "html_content", "files")
-    template_fields_renderers = {"html_content": "html"}
-    template_ext: Sequence[str] = (".html",)
-    ui_color = "#e6faf9"
-
-    def __init__(
-        self,
-        *,
-        to: list[str] | str,
-        subject: str,
-        html_content: str,
-        files: list | None = None,
-        cc: list[str] | str | None = None,
-        bcc: list[str] | str | None = None,
-        mime_subtype: str = "mixed",
-        mime_charset: str = "utf-8",
-        conn_id: str | None = None,
-        custom_headers: dict[str, Any] | None = None,
-        **kwargs,
-    ) -> None:
-        super().__init__(**kwargs)
-        self.to = to
-        self.subject = subject
-        self.html_content = html_content
-        self.files = files or []
-        self.cc = cc
-        self.bcc = bcc
-        self.mime_subtype = mime_subtype
-        self.mime_charset = mime_charset
-        self.conn_id = conn_id
-        self.custom_headers = custom_headers
-
-    def execute(self, context: Context):
-        send_email(
-            self.to,
-            self.subject,
-            self.html_content,
-            files=self.files,
-            cc=self.cc,
-            bcc=self.bcc,
-            mime_subtype=self.mime_subtype,
-            mime_charset=self.mime_charset,
-            conn_id=self.conn_id,
-            custom_headers=self.custom_headers,
-        )
diff --git a/docs/apache-airflow/core-concepts/operators.rst 
b/docs/apache-airflow/core-concepts/operators.rst
index e721faa9756..f6385ddc1b0 100644
--- a/docs/apache-airflow/core-concepts/operators.rst
+++ b/docs/apache-airflow/core-concepts/operators.rst
@@ -30,7 +30,6 @@ Airflow has a very extensive set of operators available, with 
some built-in to t
 
 - :class:`~airflow.providers.standard.operators.bash.BashOperator` - executes 
a bash command
 - :class:`~airflow.providers.standard.operators.python.PythonOperator` - calls 
an arbitrary Python function
-- :class:`~airflow.operators.email.EmailOperator` - sends an email
 - Use the ``@task`` decorator to execute an arbitrary Python function. It 
doesn't support rendering jinja templates passed as arguments.
 
 .. note::
@@ -41,6 +40,7 @@ For a list of all core operators, see: :doc:`Core Operators 
and Hooks Reference
 
 If the operator you need isn't installed with Airflow by default, you can 
probably find it as part of our huge set of community :doc:`provider packages 
<apache-airflow-providers:index>`. Some popular operators from here include:
 
+- :class:`~airflow.providers.smtp.operators.smtp.EmailOperator`
 - :class:`~airflow.providers.http.operators.http.HttpOperator`
 - :class:`~airflow.providers.common.sql.operators.sql.SQLExecuteQueryOperator`
 - :class:`~airflow.providers.docker.operators.docker.DockerOperator`
diff --git a/docs/apache-airflow/core-concepts/taskflow.rst 
b/docs/apache-airflow/core-concepts/taskflow.rst
index b31494c1558..5eef4373d33 100644
--- a/docs/apache-airflow/core-concepts/taskflow.rst
+++ b/docs/apache-airflow/core-concepts/taskflow.rst
@@ -25,7 +25,7 @@ If you write most of your DAGs using plain Python code rather 
than Operators, th
 TaskFlow takes care of moving inputs and outputs between your Tasks using 
XComs for you, as well as automatically calculating dependencies - when you 
call a TaskFlow function in your DAG file, rather than executing it, you will 
get an object representing the XCom for the result (an ``XComArg``), that you 
can then use as inputs to downstream tasks or operators. For example::
 
     from airflow.decorators import task
-    from airflow.operators.email import EmailOperator
+    from airflow.providers.email import EmailOperator
 
     @task
     def get_ip():
diff --git a/docs/apache-airflow/operators-and-hooks-ref.rst 
b/docs/apache-airflow/operators-and-hooks-ref.rst
index a0307f51f06..511775d117e 100644
--- a/docs/apache-airflow/operators-and-hooks-ref.rst
+++ b/docs/apache-airflow/operators-and-hooks-ref.rst
@@ -56,9 +56,6 @@ For details see: 
:doc:`apache-airflow-providers:operators-and-hooks-ref/index`.
    * - :mod:`airflow.providers.standard.operators.empty`
      -
 
-   * - :mod:`airflow.operators.email`
-     -
-
    * - :mod:`airflow.operators.generic_transfer`
      -
 
diff --git a/newsfragments/46572.significant.rst 
b/newsfragments/46572.significant.rst
new file mode 100644
index 00000000000..12d3feb0b4f
--- /dev/null
+++ b/newsfragments/46572.significant.rst
@@ -0,0 +1,22 @@
+In Airflow 3.0, ``airflow.operators.email.EmailOperator`` is removed.
+
+Instead, users can install ``smtp`` provider and import ``EmailOperator`` from 
the the module ``airflow.providers.smtp.operators.smtp``.
+
+* Types of change
+
+  * [ ] Dag changes
+  * [ ] Config changes
+  * [ ] API changes
+  * [ ] CLI changes
+  * [ ] Behaviour changes
+  * [ ] Plugin changes
+  * [ ] Dependency changes
+  * [x] Code interface changes
+
+* Migration rules needed
+
+  * ruff
+
+    * <RULE_ID>
+
+      * [ ] ``airflow.operators.email`` → 
``airflow.providers.smtp.operators.smtp.EmailOperator``
diff --git a/tests/operators/test_email.py b/tests/operators/test_email.py
deleted file mode 100644
index df772beb95f..00000000000
--- a/tests/operators/test_email.py
+++ /dev/null
@@ -1,62 +0,0 @@
-#
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements.  See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership.  The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied.  See the License for the
-# specific language governing permissions and limitations
-# under the License.
-from __future__ import annotations
-
-import datetime
-from unittest import mock
-
-import pytest
-
-from airflow.operators.email import EmailOperator
-from airflow.utils import timezone
-
-from tests_common.test_utils.config import conf_vars
-
-pytestmark = pytest.mark.db_test
-
-DEFAULT_DATE = timezone.datetime(2016, 1, 1)
-END_DATE = timezone.datetime(2016, 1, 2)
-INTERVAL = datetime.timedelta(hours=12)
-FROZEN_NOW = timezone.datetime(2016, 1, 2, 12, 1, 1)
-
-send_email_test = mock.Mock()
-
-
-class TestEmailOperator:
-    def test_execute(self, dag_maker):
-        with conf_vars({("email", "email_backend"): 
"tests.operators.test_email.send_email_test"}):
-            with dag_maker(
-                "test_dag",
-                default_args={"owner": "airflow", "start_date": DEFAULT_DATE},
-                schedule=INTERVAL,
-                serialized=True,
-            ):
-                task = EmailOperator(
-                    to="airf...@example.com",
-                    subject="Test Run",
-                    html_content="The quick brown fox jumps over the lazy dog",
-                    task_id="task",
-                    files=["/tmp/Report-A-{{ ds }}.csv"],
-                    custom_headers={"Reply-To": "reply...@example.com"},
-                )
-            dag_maker.create_dagrun()
-            task.run(start_date=DEFAULT_DATE, end_date=DEFAULT_DATE)
-        assert send_email_test.call_count == 1
-        call_args = send_email_test.call_args.kwargs
-        assert call_args["files"] == ["/tmp/Report-A-2016-01-01.csv"]
-        assert call_args["custom_headers"] == {"Reply-To": 
"reply...@example.com"}

Reply via email to