Repository: PrefectHQ/prefect

Issue:
EmailTask is outdated
In `prefect.tasks.notifications`, the `EmailTask` encourages users to store usernames / passwords as Task attributes; we should be using secrets.  Also, the docstring is incomplete.


Discussion:

Resolving patch:
diff --git a/src/prefect/tasks/__init__.py b/src/prefect/tasks/__init__.py
--- a/src/prefect/tasks/__init__.py
+++ b/src/prefect/tasks/__init__.py
@@ -3,6 +3,7 @@
 import prefect.tasks.control_flow
 import prefect.tasks.database
 import prefect.tasks.github
+import prefect.tasks.notifications
 import prefect.tasks.shell
 
 try:
diff --git a/src/prefect/tasks/flow_runner_task.py b/src/prefect/tasks/flow_runner_task.py
deleted file mode 100644
--- a/src/prefect/tasks/flow_runner_task.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from typing import Any
-
-import prefect
-from prefect.engine.state import State
-
-
-class FlowRunnerTask(prefect.Task):
-    def __init__(
-        self,
-        flow_runner_class: prefect.engine.flow_runner.FlowRunner = None,
-        executor: prefect.engine.executors.Executor = None,
-        **kwargs: Any
-    ):
-        self.flow_runner_class = flow_runner_class
-        self.executor = executor
-        super().__init__(**kwargs)
-
-    def run(self, flow: prefect.Flow, parameters: dict = None) -> State:  # type: ignore
-        runner_cls = (
-            self.flow_runner_class or prefect.engine.get_default_flow_runner_class()
-        )
-        flow_state = runner_cls(flow=flow).run(
-            executor=self.executor or prefect.context.get("executor"),
-            return_tasks=flow.reference_tasks(),
-            parameters=parameters or dict(),
-        )
-        return flow_state
diff --git a/src/prefect/tasks/notifications/email_task.py b/src/prefect/tasks/notifications/email_task.py
--- a/src/prefect/tasks/notifications/email_task.py
+++ b/src/prefect/tasks/notifications/email_task.py
@@ -1,38 +1,68 @@
 import smtplib
+from email.header import Header
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
 from typing import Any
 
-import prefect
+from prefect import Task
+from prefect.client import Secret
+from prefect.utilities.tasks import defaults_from_attrs
 
-EMAIL_TEMPLATE = """
-    From: {email_from}
-    To: {email_to}
-    Subject: {subject}
 
-    {msg}
+class EmailTask(Task):
     """
+    Task for sending email from an authenticated Gmail address.  For this task to function properly,
+    you must have the `"EMAIL_USERNAME"` and `"EMAIL_PASSWORD"` Prefect Secrets set.  It is recommended
+    you use a [Google App Password](https://support.google.com/accounts/answer/185833) for this purpose.
 
-
-class EmailTask(prefect.Task):
-    """
-    This task sends an email.
+    Args:
+        - subject (str, optional): the subject of the email; can also be provided at runtime
+        - msg (str, optional): the contents of the email; can also be provided at runtime
+        - email_to (str, optional): the destination email address to send the message to; can also
+            be provided at runtime
+        - **kwargs (Any, optional): additional keyword arguments to pass to the base Task initialization
     """
 
-    def __init__(self, username: str = None, password: str = None, **kwargs: Any):
+    def __init__(
+        self, subject: str = None, msg: str = None, email_to: str = None, **kwargs
+    ):
+        self.subject = subject
+        self.msg = msg
+        self.email_to = email_to
+        super().__init__(**kwargs)
 
-        self.username = username
-        self.password = password
+    @defaults_from_attrs("subject", "msg", "email_to")
+    def run(self, subject: str = None, msg: str = None, email_to: str = None):
+        """
+        Run method which sends an email.
 
-        super().__init__(**kwargs)
+        Args:
+            - subject (str, optional): the subject of the email; defaults to the one provided
+                at initialization
+            - msg (str, optional): the contents of the email; defaults to the one provided
+                at initialization
+            - email_to (str, optional): the destination email address to send the message to;
+                defaults to the one provided at initialization
 
-    def run(  # type: ignore
-        self, subject=None, msg=None, email_from=None, email_to=None
-    ):
+        Returns:
+            - None
+        """
+
+        username = Secret("EMAIL_USERNAME").get()
+        password = Secret("EMAIL_PASSWORD").get()
+
+        contents = MIMEMultipart("alternative")
+        contents.attach(MIMEText(msg, "plain"))
+
+        contents["Subject"] = Header(subject, "UTF-8")
+        contents["From"] = "notifications@prefect.io"
+        contents["To"] = email_to
 
-        message = EMAIL_TEMPLATE.format(
-            email_from=email_from, email_to=email_to, subject=subject, msg=msg
-        )
+        message = contents.as_string()
 
         server = smtplib.SMTP_SSL("smtp.gmail.com", 465)
-        server.ehlo()
-        server.login(self.username, self.password)
-        server.sendmail(from_addr=email_from, to_addrs=email_to, msg=message)
+        server.login(username, password)
+        try:
+            server.sendmail("notifications@prefect.io", email_to, message)
+        finally:
+            server.quit()


Test patch:
diff --git a/tests/tasks/notifications/__init__.py b/tests/tasks/notifications/__init__.py
new file mode 100644
diff --git a/tests/tasks/notifications/test_email_task.py b/tests/tasks/notifications/test_email_task.py
new file mode 100644
--- /dev/null
+++ b/tests/tasks/notifications/test_email_task.py
@@ -0,0 +1,27 @@
+import pytest
+from unittest.mock import MagicMock
+
+from prefect import context
+from prefect.tasks.notifications import EmailTask
+from prefect.utilities.configuration import set_temporary_config
+
+
+class TestInitialization:
+    def test_inits_with_no_args(self):
+        t = EmailTask()
+        assert t
+
+    def test_kwargs_get_passed_to_task_init(self):
+        t = EmailTask(name="bob", checkpoint=True, tags=["foo"])
+        assert t.name == "bob"
+        assert t.checkpoint is True
+        assert t.tags == {"foo"}
+
+    def test_username_password_pulled_from_secrets(self, monkeypatch):
+        smtp = MagicMock()
+        monkeypatch.setattr("prefect.tasks.notifications.email_task.smtplib", smtp)
+        t = EmailTask(msg="")
+        with set_temporary_config({"cloud.use_local_secrets": True}):
+            with context({"secrets": dict(EMAIL_USERNAME="foo", EMAIL_PASSWORD="bar")}):
+                res = t.run()
+        assert smtp.SMTP_SSL.return_value.login.call_args[0] == ("foo", "bar")