This is an automated email from the ASF dual-hosted git repository.
onikolas 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 c97532b4d73 Add `SesEmailOperator` (#58312)
c97532b4d73 is described below
commit c97532b4d73f9404c82d9d19cbaed6bd11778c62
Author: Henry Chen <[email protected]>
AuthorDate: Thu Mar 5 06:21:46 2026 +0800
Add `SesEmailOperator` (#58312)
* Add SesEmailOperator
---
docs/spelling_wordlist.txt | 1 +
providers/amazon/docs/operators/ses.rst | 84 ++++++++++
providers/amazon/provider.yaml | 7 +-
.../airflow/providers/amazon/aws/operators/ses.py | 143 ++++++++++++++++
.../airflow/providers/amazon/get_provider_info.py | 7 +-
.../amazon/tests/system/amazon/aws/example_ses.py | 118 ++++++++++++++
.../tests/unit/amazon/aws/operators/test_ses.py | 179 +++++++++++++++++++++
7 files changed, 537 insertions(+), 2 deletions(-)
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 36104f629a7..ad1fe18c75b 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -1670,6 +1670,7 @@ ServicePrincipalCredentials
ServiceResource
ServicesClient
SES
+ses
sessionmaker
setattr
setdefault
diff --git a/providers/amazon/docs/operators/ses.rst
b/providers/amazon/docs/operators/ses.rst
new file mode 100644
index 00000000000..fed0230c098
--- /dev/null
+++ b/providers/amazon/docs/operators/ses.rst
@@ -0,0 +1,84 @@
+ .. 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.
+
+====================================
+Amazon Simple Email Service (SES)
+====================================
+
+`Amazon Simple Email Service (Amazon SES) <https://aws.amazon.com/ses/>`__ is
a cloud email
+service provider that can integrate into any application for bulk email
sending. Whether you
+send transactional or marketing emails, you pay only for what you use. Amazon
SES also supports
+a variety of deployments including dedicated, shared, or owned IP addresses.
Reports on sender
+statistics and a deliverability dashboard help businesses make every email
count.
+
+Prerequisite Tasks
+------------------
+
+.. include:: ../_partials/prerequisite_tasks.rst
+
+Generic Parameters
+------------------
+
+.. include:: ../_partials/generic_parameters.rst
+
+Operators
+---------
+
+.. _howto/operator:SesEmailOperator:
+
+Send an email using Amazon SES
+===============================
+
+To send an email using Amazon Simple Email Service you can use
+:class:`~airflow.providers.amazon.aws.operators.ses.SesEmailOperator`.
+
+The following example shows how to send a basic email:
+
+.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_ses.py
+ :language: python
+ :dedent: 4
+ :start-after: [START howto_operator_ses_email_basic]
+ :end-before: [END howto_operator_ses_email_basic]
+
+You can also send emails with CC and BCC recipients:
+
+.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_ses.py
+ :language: python
+ :dedent: 4
+ :start-after: [START howto_operator_ses_email_cc_bcc]
+ :end-before: [END howto_operator_ses_email_cc_bcc]
+
+For more advanced use cases, you can add custom headers and set reply-to
addresses:
+
+.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_ses.py
+ :language: python
+ :dedent: 4
+ :start-after: [START howto_operator_ses_email_headers]
+ :end-before: [END howto_operator_ses_email_headers]
+
+The operator also supports Jinja templating for dynamic content:
+
+.. exampleinclude:: /../../amazon/tests/system/amazon/aws/example_ses.py
+ :language: python
+ :dedent: 4
+ :start-after: [START howto_operator_ses_email_templated]
+ :end-before: [END howto_operator_ses_email_templated]
+
+Reference
+---------
+
+* `AWS boto3 library documentation for SES
<https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ses.html>`__
diff --git a/providers/amazon/provider.yaml b/providers/amazon/provider.yaml
index 02cb30a1fd0..dee23e4dd86 100644
--- a/providers/amazon/provider.yaml
+++ b/providers/amazon/provider.yaml
@@ -176,6 +176,8 @@ integrations:
- integration-name: Amazon ECS
external-doc-url: https://aws.amazon.com/ecs/
logo:
/docs/integration-logos/[email protected]
+ how-to-guide:
+ - /docs/apache-airflow-providers-amazon/operators/ecs.rst
tags: [aws]
- integration-name: Amazon Elastic Kubernetes Service (EKS)
external-doc-url: https://aws.amazon.com/eks/
@@ -272,7 +274,7 @@ integrations:
external-doc-url: https://aws.amazon.com/ses/
logo:
/docs/integration-logos/[email protected]
how-to-guide:
- - /docs/apache-airflow-providers-amazon/operators/ecs.rst
+ - /docs/apache-airflow-providers-amazon/operators/ses.rst
tags: [aws]
- integration-name: Amazon Simple Notification Service (SNS)
external-doc-url: https://aws.amazon.com/sns/
@@ -450,6 +452,9 @@ operators:
- integration-name: Amazon SageMaker Unified Studio
python-modules:
- airflow.providers.amazon.aws.operators.sagemaker_unified_studio
+ - integration-name: Amazon Simple Email Service (SES)
+ python-modules:
+ - airflow.providers.amazon.aws.operators.ses
- integration-name: Amazon Simple Notification Service (SNS)
python-modules:
- airflow.providers.amazon.aws.operators.sns
diff --git a/providers/amazon/src/airflow/providers/amazon/aws/operators/ses.py
b/providers/amazon/src/airflow/providers/amazon/aws/operators/ses.py
new file mode 100644
index 00000000000..adf57f153fa
--- /dev/null
+++ b/providers/amazon/src/airflow/providers/amazon/aws/operators/ses.py
@@ -0,0 +1,143 @@
+# 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.
+"""Send email using Amazon Simple Email Service (SES)."""
+
+from __future__ import annotations
+
+from collections.abc import Iterable, Sequence
+from typing import TYPE_CHECKING, Any
+
+from airflow.providers.amazon.aws.hooks.ses import SesHook
+from airflow.providers.amazon.aws.operators.base_aws import AwsBaseOperator
+from airflow.providers.amazon.aws.utils.mixins import aws_template_fields
+
+if TYPE_CHECKING:
+ from airflow.sdk import Context
+
+
+class SesEmailOperator(AwsBaseOperator[SesHook]):
+ """
+ Send an email using Amazon Simple Email Service (SES).
+
+ .. seealso::
+ For more information on how to use this operator, take a look at the
guide:
+ :ref:`howto/operator:SesEmailOperator`
+
+ :param mail_from: Email address to set as email's from (templated)
+ :param to: List of email addresses to set as email's to (templated)
+ :param subject: Email's subject (templated)
+ :param html_content: Content of email in HTML format (templated)
+ :param files: List of paths of files to be attached
+ :param cc: List of email addresses to set as email's CC (templated)
+ :param bcc: List of email addresses to set as email's BCC (templated)
+ :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 reply_to: The email address to which replies will be sent
+ :param return_path: The email address to which message bounces and
complaints should be sent
+ :param custom_headers: Additional headers to add to the MIME message
+ :param aws_conn_id: The Airflow connection used for AWS credentials.
+ If this is ``None`` or empty then the default boto3 behaviour is used.
If
+ running Airflow in a distributed manner and aws_conn_id is None or
+ empty, then default boto3 configuration would be used (and must be
+ maintained on each worker node).
+ :param region_name: AWS region_name. If not specified then the default
boto3 behaviour is used.
+ :param verify: Whether or not to verify SSL certificates. See:
+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/core/session.html
+ :param botocore_config: Configuration dictionary (key-values) for botocore
client. See:
+
https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html
+ """
+
+ aws_hook_class = SesHook
+ template_fields: Sequence[str] = aws_template_fields(
+ "mail_from",
+ "to",
+ "subject",
+ "html_content",
+ "cc",
+ "bcc",
+ "mime_subtype",
+ "mime_charset",
+ "reply_to",
+ "return_path",
+ "custom_headers",
+ )
+ template_fields_renderers = {
+ "custom_headers": "json",
+ }
+
+ def __init__(
+ self,
+ *,
+ mail_from: str,
+ to: str | Iterable[str],
+ subject: str,
+ html_content: str,
+ files: list[str] | None = None,
+ cc: str | Iterable[str] | None = None,
+ bcc: str | Iterable[str] | None = None,
+ mime_subtype: str = "mixed",
+ mime_charset: str = "utf-8",
+ reply_to: str | None = None,
+ return_path: str | None = None,
+ custom_headers: dict[str, Any] | None = None,
+ **kwargs,
+ ):
+ super().__init__(**kwargs)
+ self.mail_from = mail_from
+ self.to = to
+ self.subject = subject
+ self.html_content = html_content
+ self.files = files
+ self.cc = cc
+ self.bcc = bcc
+ self.mime_subtype = mime_subtype
+ self.mime_charset = mime_charset
+ self.reply_to = reply_to
+ self.return_path = return_path
+ self.custom_headers = custom_headers
+
+ def execute(self, context: Context) -> dict:
+ """
+ Send email using Amazon SES.
+
+ :param context: The task context
+ :return: Response from Amazon SES service with unique message
identifier
+ """
+ self.log.info(
+ "Sending email via Amazon SES from %s to %s with subject: %s",
+ self.mail_from,
+ self.to,
+ self.subject,
+ )
+
+ response = self.hook.send_email(
+ mail_from=self.mail_from,
+ to=self.to,
+ subject=self.subject,
+ html_content=self.html_content,
+ files=self.files,
+ cc=self.cc,
+ bcc=self.bcc,
+ mime_subtype=self.mime_subtype,
+ mime_charset=self.mime_charset,
+ reply_to=self.reply_to,
+ return_path=self.return_path,
+ custom_headers=self.custom_headers,
+ )
+
+ self.log.info("Email sent successfully. Message ID: %s",
response.get("MessageId"))
+ return response
diff --git a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
index fbdd7ef45de..4c52781f42e 100644
--- a/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
+++ b/providers/amazon/src/airflow/providers/amazon/get_provider_info.py
@@ -100,6 +100,7 @@ def get_provider_info():
"integration-name": "Amazon ECS",
"external-doc-url": "https://aws.amazon.com/ecs/",
"logo":
"/docs/integration-logos/[email protected]",
+ "how-to-guide":
["/docs/apache-airflow-providers-amazon/operators/ecs.rst"],
"tags": ["aws"],
},
{
@@ -222,7 +223,7 @@ def get_provider_info():
"integration-name": "Amazon Simple Email Service (SES)",
"external-doc-url": "https://aws.amazon.com/ses/",
"logo":
"/docs/integration-logos/[email protected]",
- "how-to-guide":
["/docs/apache-airflow-providers-amazon/operators/ecs.rst"],
+ "how-to-guide":
["/docs/apache-airflow-providers-amazon/operators/ses.rst"],
"tags": ["aws"],
},
{
@@ -447,6 +448,10 @@ def get_provider_info():
"integration-name": "Amazon SageMaker Unified Studio",
"python-modules":
["airflow.providers.amazon.aws.operators.sagemaker_unified_studio"],
},
+ {
+ "integration-name": "Amazon Simple Email Service (SES)",
+ "python-modules":
["airflow.providers.amazon.aws.operators.ses"],
+ },
{
"integration-name": "Amazon Simple Notification Service (SNS)",
"python-modules":
["airflow.providers.amazon.aws.operators.sns"],
diff --git a/providers/amazon/tests/system/amazon/aws/example_ses.py
b/providers/amazon/tests/system/amazon/aws/example_ses.py
new file mode 100644
index 00000000000..df1d0d92ee1
--- /dev/null
+++ b/providers/amazon/tests/system/amazon/aws/example_ses.py
@@ -0,0 +1,118 @@
+# 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 datetime import datetime
+
+from airflow.providers.amazon.aws.operators.ses import SesEmailOperator
+
+from tests_common.test_utils.version_compat import AIRFLOW_V_3_0_PLUS
+
+if AIRFLOW_V_3_0_PLUS:
+ from airflow.sdk import DAG, chain
+else:
+ # Airflow 2 path
+ from airflow.models.baseoperator import chain # type:
ignore[attr-defined,no-redef]
+ from airflow.models.dag import DAG # type:
ignore[attr-defined,no-redef,assignment]
+
+from system.amazon.aws.utils import SystemTestContextBuilder
+
+SES_VERIFIED_EMAIL_KEY = "SES_VERIFIED_EMAIL"
+sys_test_context_task =
SystemTestContextBuilder().add_variable(SES_VERIFIED_EMAIL_KEY).build()
+
+with DAG(
+ dag_id="example_ses",
+ start_date=datetime(2021, 1, 1),
+ schedule="@once",
+ catchup=False,
+ tags=["example"],
+) as dag:
+ test_context = sys_test_context_task()
+ verified_email = test_context[SES_VERIFIED_EMAIL_KEY]
+
+ # [START howto_operator_ses_email_basic]
+ # Basic email sending
+ # Note: In SES sandbox mode, both sender and recipient must be verified.
+ send_basic_email = SesEmailOperator(
+ task_id="send_basic_email",
+ mail_from=verified_email,
+ to=[verified_email],
+ subject="Test Email from Airflow",
+ html_content="<h1>Hello</h1><p>This is a test email sent via Amazon
SES.</p>",
+ aws_conn_id="aws_default",
+ )
+ # [END howto_operator_ses_email_basic]
+
+ # [START howto_operator_ses_email_cc_bcc]
+ # Email with CC and BCC
+ send_email_with_cc_bcc = SesEmailOperator(
+ task_id="send_email_with_cc_bcc",
+ mail_from=verified_email,
+ to=[verified_email],
+ cc=[verified_email],
+ bcc=[verified_email],
+ subject="Test Email with CC and BCC",
+ html_content="<h1>Hello</h1><p>This email has CC and BCC
recipients.</p>",
+ aws_conn_id="aws_default",
+ )
+ # [END howto_operator_ses_email_cc_bcc]
+
+ # [START howto_operator_ses_email_headers]
+ # Email with custom headers and reply-to
+ send_email_with_headers = SesEmailOperator(
+ task_id="send_email_with_headers",
+ mail_from=verified_email,
+ to=[verified_email],
+ subject="Test Email with Custom Headers",
+ html_content="<h1>Hello</h1><p>This email has custom headers.</p>",
+ reply_to=verified_email,
+ return_path=verified_email,
+ custom_headers={"X-Custom-Header": "CustomValue"},
+ aws_conn_id="aws_default",
+ )
+ # [END howto_operator_ses_email_headers]
+
+ # [START howto_operator_ses_email_templated]
+ # Email with template variables
+ send_templated_email = SesEmailOperator(
+ task_id="send_templated_email",
+ mail_from=verified_email,
+ to=[verified_email],
+ subject="DAG Run: {{ dag.dag_id }} - {{ ds }}",
+ html_content="""
+ <h1>DAG Run Report</h1>
+ <p>DAG ID: {{ dag.dag_id }}</p>
+ <p>Execution Date: {{ ds }}</p>
+ <p>Run ID: {{ run_id }}</p>
+ """,
+ aws_conn_id="aws_default",
+ )
+ # [END howto_operator_ses_email_templated]
+
+ chain(
+ test_context,
+ send_basic_email,
+ send_email_with_cc_bcc,
+ send_email_with_headers,
+ send_templated_email,
+ )
+
+
+from tests_common.test_utils.system_tests import get_test_run # noqa: E402
+
+# Needed to run the example DAG with pytest (see:
tests/system/README.md#run_via_pytest)
+test_run = get_test_run(dag)
diff --git a/providers/amazon/tests/unit/amazon/aws/operators/test_ses.py
b/providers/amazon/tests/unit/amazon/aws/operators/test_ses.py
new file mode 100644
index 00000000000..12944078aac
--- /dev/null
+++ b/providers/amazon/tests/unit/amazon/aws/operators/test_ses.py
@@ -0,0 +1,179 @@
+#
+# 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 unittest import mock
+
+import pytest
+
+from airflow.providers.amazon.aws.operators.ses import SesEmailOperator
+
+from unit.amazon.aws.utils.test_template_fields import validate_template_fields
+
+TASK_ID = "ses_email_job"
+AWS_CONN_ID = "custom_aws_conn"
+MAIL_FROM = "[email protected]"
+TO = ["[email protected]"]
+SUBJECT = "Test Subject"
+HTML_CONTENT = "<h1>Test Email</h1><p>This is a test.</p>"
+
+
+class TestSesEmailOperator:
+ @pytest.fixture(autouse=True)
+ def _setup_test_cases(self):
+ self.default_op_kwargs = {
+ "task_id": TASK_ID,
+ "mail_from": MAIL_FROM,
+ "to": TO,
+ "subject": SUBJECT,
+ "html_content": HTML_CONTENT,
+ }
+
+ def test_init(self):
+ """Test operator initialization with default parameters."""
+ op = SesEmailOperator(**self.default_op_kwargs)
+ assert op.hook.aws_conn_id == "aws_default"
+ assert op.hook._region_name is None
+ assert op.hook._verify is None
+ assert op.hook._config is None
+ assert op.mail_from == MAIL_FROM
+ assert op.to == TO
+ assert op.subject == SUBJECT
+ assert op.html_content == HTML_CONTENT
+
+ def test_init_with_custom_params(self):
+ """Test operator initialization with custom AWS parameters."""
+ op = SesEmailOperator(
+ **self.default_op_kwargs,
+ aws_conn_id=AWS_CONN_ID,
+ region_name="us-west-1",
+ verify="/spam/egg.pem",
+ botocore_config={"read_timeout": 42},
+ cc=["[email protected]"],
+ bcc=["[email protected]"],
+ reply_to="[email protected]",
+ return_path="[email protected]",
+ )
+ assert op.hook.aws_conn_id == AWS_CONN_ID
+ assert op.hook._region_name == "us-west-1"
+ assert op.hook._verify == "/spam/egg.pem"
+ assert op.hook._config is not None
+ assert op.hook._config.read_timeout == 42
+ assert op.cc == ["[email protected]"]
+ assert op.bcc == ["[email protected]"]
+ assert op.reply_to == "[email protected]"
+ assert op.return_path == "[email protected]"
+
+ @mock.patch.object(SesEmailOperator, "hook")
+ def test_execute_basic(self, mocked_hook):
+ """Test basic email sending execution."""
+ hook_response = {"MessageId": "test-message-id-123"}
+ mocked_hook.send_email.return_value = hook_response
+
+ op = SesEmailOperator(**self.default_op_kwargs)
+ result = op.execute({})
+
+ assert result == hook_response
+ mocked_hook.send_email.assert_called_once_with(
+ mail_from=MAIL_FROM,
+ to=TO,
+ subject=SUBJECT,
+ html_content=HTML_CONTENT,
+ files=None,
+ cc=None,
+ bcc=None,
+ mime_subtype="mixed",
+ mime_charset="utf-8",
+ reply_to=None,
+ return_path=None,
+ custom_headers=None,
+ )
+
+ @mock.patch.object(SesEmailOperator, "hook")
+ def test_execute_with_all_params(self, mocked_hook):
+ """Test email sending with all optional parameters."""
+ hook_response = {"MessageId": "test-message-id-456"}
+ mocked_hook.send_email.return_value = hook_response
+
+ custom_headers = {"X-Custom-Header": "CustomValue"}
+ files = ["/path/to/file1.txt", "/path/to/file2.pdf"]
+
+ op = SesEmailOperator(
+ **self.default_op_kwargs,
+ cc=["[email protected]"],
+ bcc=["[email protected]"],
+ files=files,
+ mime_subtype="alternative",
+ mime_charset="iso-8859-1",
+ reply_to="[email protected]",
+ return_path="[email protected]",
+ custom_headers=custom_headers,
+ )
+ result = op.execute({})
+
+ assert result == hook_response
+ mocked_hook.send_email.assert_called_once_with(
+ mail_from=MAIL_FROM,
+ to=TO,
+ subject=SUBJECT,
+ html_content=HTML_CONTENT,
+ files=files,
+ cc=["[email protected]"],
+ bcc=["[email protected]"],
+ mime_subtype="alternative",
+ mime_charset="iso-8859-1",
+ reply_to="[email protected]",
+ return_path="[email protected]",
+ custom_headers=custom_headers,
+ )
+
+ @mock.patch.object(SesEmailOperator, "hook")
+ def test_execute_with_string_to(self, mocked_hook):
+ """Test email sending with 'to' as a string instead of list."""
+ hook_response = {"MessageId": "test-message-id-789"}
+ mocked_hook.send_email.return_value = hook_response
+
+ op = SesEmailOperator(
+ task_id=TASK_ID,
+ mail_from=MAIL_FROM,
+ to="[email protected]",
+ subject=SUBJECT,
+ html_content=HTML_CONTENT,
+ )
+ result = op.execute({})
+
+ assert result == hook_response
+ mocked_hook.send_email.assert_called_once_with(
+ mail_from=MAIL_FROM,
+ to="[email protected]",
+ subject=SUBJECT,
+ html_content=HTML_CONTENT,
+ files=None,
+ cc=None,
+ bcc=None,
+ mime_subtype="mixed",
+ mime_charset="utf-8",
+ reply_to=None,
+ return_path=None,
+ custom_headers=None,
+ )
+
+ def test_template_fields(self):
+ """Test that template fields are properly configured."""
+ operator = SesEmailOperator(**self.default_op_kwargs)
+ validate_template_fields(operator)