* feat: add ses v2

* feat: move wrap v1 logic into v2 api

* feat: v2 api on v2 url

* chore: types

* chore: linting

* feat: raw emails

* chore: linting

* feat: add list_contacts

* fix: urls need to be explicit for this to work with moto server

* chore: linting

* chore: remodel

* chore: rework

* chore: cleanup

* chore: fix test

* chore: sort out mypy

* feat: add contact lists

* fix: new url for server mode

* feat: create and delete

* chore: linting

* chore: linting

* chore: refactor

* chore: match errors with real responses

* chore: linting

* chore: run implementation coverage script

* refactor: easier, faster look ups with dicts

* refactor: contact is now a child of contactlist

* tests: contactlists return 200 if empty, contacts do not

* chore: update botocore and run implementation coverage script

* refactor: add matching *_contact methods to backend model so coverage script can detect them
This commit is contained in:
rafcio19 2023-05-01 19:15:29 +01:00 committed by GitHub
parent 65cf66dbcf
commit 6f170410e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 923 additions and 27 deletions

View File

@ -358,11 +358,13 @@
## athena
<details>
<summary>26% implemented</summary>
<summary>23% implemented</summary>
- [ ] batch_get_named_query
- [ ] batch_get_prepared_statement
- [ ] batch_get_query_execution
- [ ] cancel_capacity_reservation
- [ ] create_capacity_reservation
- [X] create_data_catalog
- [X] create_named_query
- [ ] create_notebook
@ -378,6 +380,8 @@
- [ ] get_calculation_execution
- [ ] get_calculation_execution_code
- [ ] get_calculation_execution_status
- [ ] get_capacity_assignment_configuration
- [ ] get_capacity_reservation
- [X] get_data_catalog
- [ ] get_database
- [X] get_named_query
@ -393,6 +397,7 @@
- [ ] import_notebook
- [ ] list_application_dpu_sizes
- [ ] list_calculation_executions
- [ ] list_capacity_reservations
- [X] list_data_catalogs
- [ ] list_databases
- [ ] list_engine_versions
@ -406,6 +411,7 @@
- [ ] list_table_metadata
- [ ] list_tags_for_resource
- [X] list_work_groups
- [ ] put_capacity_assignment_configuration
- [ ] start_calculation_execution
- [X] start_query_execution
- [ ] start_session
@ -414,6 +420,7 @@
- [ ] tag_resource
- [ ] terminate_session
- [ ] untag_resource
- [ ] update_capacity_reservation
- [ ] update_data_catalog
- [ ] update_named_query
- [ ] update_notebook
@ -1453,8 +1460,9 @@
## datasync
<details>
<summary>13% implemented</summary>
<summary>10% implemented</summary>
- [ ] add_storage_system
- [X] cancel_task_execution
- [ ] create_agent
- [ ] create_location_efs
@ -1472,6 +1480,7 @@
- [X] delete_location
- [X] delete_task
- [ ] describe_agent
- [ ] describe_discovery_job
- [ ] describe_location_efs
- [ ] describe_location_fsx_lustre
- [ ] describe_location_fsx_ontap
@ -1482,21 +1491,32 @@
- [ ] describe_location_object_storage
- [ ] describe_location_s3
- [ ] describe_location_smb
- [ ] describe_storage_system
- [ ] describe_storage_system_resource_metrics
- [ ] describe_storage_system_resources
- [ ] describe_task
- [ ] describe_task_execution
- [ ] generate_recommendations
- [ ] list_agents
- [ ] list_discovery_jobs
- [ ] list_locations
- [ ] list_storage_systems
- [ ] list_tags_for_resource
- [ ] list_task_executions
- [ ] list_tasks
- [ ] remove_storage_system
- [ ] start_discovery_job
- [X] start_task_execution
- [ ] stop_discovery_job
- [ ] tag_resource
- [ ] untag_resource
- [ ] update_agent
- [ ] update_discovery_job
- [ ] update_location_hdfs
- [ ] update_location_nfs
- [ ] update_location_object_storage
- [ ] update_location_smb
- [ ] update_storage_system
- [X] update_task
- [ ] update_task_execution
</details>
@ -2831,7 +2851,7 @@
## emr-containers
<details>
<summary>42% implemented</summary>
<summary>40% implemented</summary>
- [X] cancel_job_run
- [ ] create_job_template
@ -2844,6 +2864,7 @@
- [ ] describe_job_template
- [ ] describe_managed_endpoint
- [X] describe_virtual_cluster
- [ ] get_managed_endpoint_session_credentials
- [X] list_job_runs
- [ ] list_job_templates
- [ ] list_managed_endpoints
@ -3121,7 +3142,7 @@
## glue
<details>
<summary>26% implemented</summary>
<summary>30% implemented</summary>
- [X] batch_create_partition
- [ ] batch_delete_connection
@ -3135,7 +3156,7 @@
- [ ] batch_get_dev_endpoints
- [X] batch_get_jobs
- [X] batch_get_partition
- [ ] batch_get_triggers
- [X] batch_get_triggers
- [ ] batch_get_workflows
- [ ] batch_stop_job_run
- [X] batch_update_partition
@ -3162,7 +3183,7 @@
- [ ] create_security_configuration
- [ ] create_session
- [X] create_table
- [ ] create_trigger
- [X] create_trigger
- [ ] create_user_defined_function
- [ ] create_workflow
- [ ] delete_blueprint
@ -3187,7 +3208,7 @@
- [ ] delete_session
- [X] delete_table
- [X] delete_table_version
- [ ] delete_trigger
- [X] delete_trigger
- [ ] delete_user_defined_function
- [ ] delete_workflow
- [ ] get_blueprint
@ -3244,8 +3265,8 @@
- [X] get_table_versions
- [X] get_tables
- [X] get_tags
- [ ] get_trigger
- [ ] get_triggers
- [X] get_trigger
- [X] get_triggers
- [ ] get_unfiltered_partition_metadata
- [ ] get_unfiltered_partitions_metadata
- [ ] get_unfiltered_table_metadata
@ -3272,7 +3293,7 @@
- [ ] list_schemas
- [ ] list_sessions
- [ ] list_statements
- [ ] list_triggers
- [X] list_triggers
- [ ] list_workflows
- [ ] put_data_catalog_encryption_settings
- [ ] put_resource_policy
@ -3295,12 +3316,12 @@
- [X] start_job_run
- [ ] start_ml_evaluation_task_run
- [ ] start_ml_labeling_set_generation_task_run
- [ ] start_trigger
- [X] start_trigger
- [ ] start_workflow_run
- [X] stop_crawler
- [ ] stop_crawler_schedule
- [ ] stop_session
- [ ] stop_trigger
- [X] stop_trigger
- [ ] stop_workflow_run
- [X] tag_resource
- [X] untag_resource
@ -3482,6 +3503,7 @@
- [ ] list_publishing_destinations
- [ ] list_tags_for_resource
- [ ] list_threat_intel_sets
- [ ] start_malware_scan
- [ ] start_monitoring_members
- [ ] stop_monitoring_members
- [ ] tag_resource
@ -4863,7 +4885,7 @@
## pinpoint
<details>
<summary>10% implemented</summary>
<summary>9% implemented</summary>
- [X] create_app
- [ ] create_campaign
@ -4932,6 +4954,9 @@
- [ ] get_journey_date_range_kpi
- [ ] get_journey_execution_activity_metrics
- [ ] get_journey_execution_metrics
- [ ] get_journey_run_execution_activity_metrics
- [ ] get_journey_run_execution_metrics
- [ ] get_journey_runs
- [ ] get_push_template
- [ ] get_recommender_configuration
- [ ] get_recommender_configurations
@ -6412,6 +6437,98 @@
- [X] verify_email_identity
</details>
## sesv2
<details>
<summary>10% implemented</summary>
- [ ] batch_get_metric_data
- [ ] create_configuration_set
- [ ] create_configuration_set_event_destination
- [X] create_contact
- [X] create_contact_list
- [ ] create_custom_verification_email_template
- [ ] create_dedicated_ip_pool
- [ ] create_deliverability_test_report
- [ ] create_email_identity
- [ ] create_email_identity_policy
- [ ] create_email_template
- [ ] create_import_job
- [ ] delete_configuration_set
- [ ] delete_configuration_set_event_destination
- [X] delete_contact
- [X] delete_contact_list
- [ ] delete_custom_verification_email_template
- [ ] delete_dedicated_ip_pool
- [ ] delete_email_identity
- [ ] delete_email_identity_policy
- [ ] delete_email_template
- [ ] delete_suppressed_destination
- [ ] get_account
- [ ] get_blacklist_reports
- [ ] get_configuration_set
- [ ] get_configuration_set_event_destinations
- [X] get_contact
- [X] get_contact_list
- [ ] get_custom_verification_email_template
- [ ] get_dedicated_ip
- [ ] get_dedicated_ip_pool
- [ ] get_dedicated_ips
- [ ] get_deliverability_dashboard_options
- [ ] get_deliverability_test_report
- [ ] get_domain_deliverability_campaign
- [ ] get_domain_statistics_report
- [ ] get_email_identity
- [ ] get_email_identity_policies
- [ ] get_email_template
- [ ] get_import_job
- [ ] get_suppressed_destination
- [ ] list_configuration_sets
- [X] list_contact_lists
- [X] list_contacts
- [ ] list_custom_verification_email_templates
- [ ] list_dedicated_ip_pools
- [ ] list_deliverability_test_reports
- [ ] list_domain_deliverability_campaigns
- [ ] list_email_identities
- [ ] list_email_templates
- [ ] list_import_jobs
- [ ] list_recommendations
- [ ] list_suppressed_destinations
- [ ] list_tags_for_resource
- [ ] put_account_dedicated_ip_warmup_attributes
- [ ] put_account_details
- [ ] put_account_sending_attributes
- [ ] put_account_suppression_attributes
- [ ] put_account_vdm_attributes
- [ ] put_configuration_set_delivery_options
- [ ] put_configuration_set_reputation_options
- [ ] put_configuration_set_sending_options
- [ ] put_configuration_set_suppression_options
- [ ] put_configuration_set_tracking_options
- [ ] put_configuration_set_vdm_options
- [ ] put_dedicated_ip_in_pool
- [ ] put_dedicated_ip_warmup_attributes
- [ ] put_deliverability_dashboard_option
- [ ] put_email_identity_configuration_set_attributes
- [ ] put_email_identity_dkim_attributes
- [ ] put_email_identity_dkim_signing_attributes
- [ ] put_email_identity_feedback_attributes
- [ ] put_email_identity_mail_from_attributes
- [ ] put_suppressed_destination
- [ ] send_bulk_email
- [ ] send_custom_verification_email
- [X] send_email
- [ ] tag_resource
- [ ] test_render_email_template
- [ ] untag_resource
- [ ] update_configuration_set_event_destination
- [ ] update_contact
- [ ] update_contact_list
- [ ] update_custom_verification_email_template
- [ ] update_email_identity_policy
- [ ] update_email_template
</details>
## signer
<details>
<summary>23% implemented</summary>
@ -7113,6 +7230,7 @@
- omics
- opensearchserverless
- opsworkscm
- osis
- outposts
- panorama
- personalize-events
@ -7152,7 +7270,6 @@
- serverlessrepo
- servicecatalog
- servicecatalog-appregistry
- sesv2
- shield
- simspaceweaver
- sms

View File

@ -28,6 +28,8 @@ athena
- [ ] batch_get_named_query
- [ ] batch_get_prepared_statement
- [ ] batch_get_query_execution
- [ ] cancel_capacity_reservation
- [ ] create_capacity_reservation
- [X] create_data_catalog
- [X] create_named_query
- [ ] create_notebook
@ -43,6 +45,8 @@ athena
- [ ] get_calculation_execution
- [ ] get_calculation_execution_code
- [ ] get_calculation_execution_status
- [ ] get_capacity_assignment_configuration
- [ ] get_capacity_reservation
- [X] get_data_catalog
- [ ] get_database
- [X] get_named_query
@ -104,6 +108,7 @@ athena
- [ ] import_notebook
- [ ] list_application_dpu_sizes
- [ ] list_calculation_executions
- [ ] list_capacity_reservations
- [X] list_data_catalogs
- [ ] list_databases
- [ ] list_engine_versions
@ -117,6 +122,7 @@ athena
- [ ] list_table_metadata
- [ ] list_tags_for_resource
- [X] list_work_groups
- [ ] put_capacity_assignment_configuration
- [ ] start_calculation_execution
- [X] start_query_execution
- [ ] start_session
@ -125,6 +131,7 @@ athena
- [ ] tag_resource
- [ ] terminate_session
- [ ] untag_resource
- [ ] update_capacity_reservation
- [ ] update_data_catalog
- [ ] update_named_query
- [ ] update_notebook

View File

@ -25,6 +25,7 @@ datasync
|start-h3| Implemented features for this service |end-h3|
- [ ] add_storage_system
- [X] cancel_task_execution
- [ ] create_agent
- [ ] create_location_efs
@ -42,6 +43,7 @@ datasync
- [X] delete_location
- [X] delete_task
- [ ] describe_agent
- [ ] describe_discovery_job
- [ ] describe_location_efs
- [ ] describe_location_fsx_lustre
- [ ] describe_location_fsx_ontap
@ -52,21 +54,32 @@ datasync
- [ ] describe_location_object_storage
- [ ] describe_location_s3
- [ ] describe_location_smb
- [ ] describe_storage_system
- [ ] describe_storage_system_resource_metrics
- [ ] describe_storage_system_resources
- [ ] describe_task
- [ ] describe_task_execution
- [ ] generate_recommendations
- [ ] list_agents
- [ ] list_discovery_jobs
- [ ] list_locations
- [ ] list_storage_systems
- [ ] list_tags_for_resource
- [ ] list_task_executions
- [ ] list_tasks
- [ ] remove_storage_system
- [ ] start_discovery_job
- [X] start_task_execution
- [ ] stop_discovery_job
- [ ] tag_resource
- [ ] untag_resource
- [ ] update_agent
- [ ] update_discovery_job
- [ ] update_location_hdfs
- [ ] update_location_nfs
- [ ] update_location_object_storage
- [ ] update_location_smb
- [ ] update_storage_system
- [X] update_task
- [ ] update_task_execution

View File

@ -38,6 +38,7 @@ emr-containers
- [ ] describe_job_template
- [ ] describe_managed_endpoint
- [X] describe_virtual_cluster
- [ ] get_managed_endpoint_session_credentials
- [X] list_job_runs
- [ ] list_job_templates
- [ ] list_managed_endpoints

View File

@ -37,7 +37,7 @@ glue
- [ ] batch_get_dev_endpoints
- [X] batch_get_jobs
- [X] batch_get_partition
- [ ] batch_get_triggers
- [X] batch_get_triggers
- [ ] batch_get_workflows
- [ ] batch_stop_job_run
- [X] batch_update_partition
@ -68,7 +68,7 @@ glue
- [ ] create_security_configuration
- [ ] create_session
- [X] create_table
- [ ] create_trigger
- [X] create_trigger
- [ ] create_user_defined_function
- [ ] create_workflow
- [ ] delete_blueprint
@ -93,7 +93,7 @@ glue
- [ ] delete_session
- [X] delete_table
- [X] delete_table_version
- [ ] delete_trigger
- [X] delete_trigger
- [ ] delete_user_defined_function
- [ ] delete_workflow
- [ ] get_blueprint
@ -162,8 +162,8 @@ glue
- [X] get_table_versions
- [X] get_tables
- [X] get_tags
- [ ] get_trigger
- [ ] get_triggers
- [X] get_trigger
- [X] get_triggers
- [ ] get_unfiltered_partition_metadata
- [ ] get_unfiltered_partitions_metadata
- [ ] get_unfiltered_table_metadata
@ -190,7 +190,7 @@ glue
- [ ] list_schemas
- [ ] list_sessions
- [ ] list_statements
- [ ] list_triggers
- [X] list_triggers
- [ ] list_workflows
- [ ] put_data_catalog_encryption_settings
- [ ] put_resource_policy
@ -213,12 +213,12 @@ glue
- [X] start_job_run
- [ ] start_ml_evaluation_task_run
- [ ] start_ml_labeling_set_generation_task_run
- [ ] start_trigger
- [X] start_trigger
- [ ] start_workflow_run
- [X] stop_crawler
- [ ] stop_crawler_schedule
- [ ] stop_session
- [ ] stop_trigger
- [X] stop_trigger
- [ ] stop_workflow_run
- [X] tag_resource
- [X] untag_resource

View File

@ -86,6 +86,7 @@ guardduty
- [ ] list_publishing_destinations
- [ ] list_tags_for_resource
- [ ] list_threat_intel_sets
- [ ] start_malware_scan
- [ ] start_monitoring_members
- [ ] stop_monitoring_members
- [ ] tag_resource

View File

@ -98,6 +98,9 @@ pinpoint
- [ ] get_journey_date_range_kpi
- [ ] get_journey_execution_activity_metrics
- [ ] get_journey_execution_metrics
- [ ] get_journey_run_execution_activity_metrics
- [ ] get_journey_run_execution_metrics
- [ ] get_journey_runs
- [ ] get_push_template
- [ ] get_recommender_configuration
- [ ] get_recommender_configurations

View File

@ -0,0 +1,116 @@
.. _implementedservice_sesv2:
.. |start-h3| raw:: html
<h3>
.. |end-h3| raw:: html
</h3>
=====
sesv2
=====
.. autoclass:: moto.sesv2.models.SESV2Backend
|start-h3| Example usage |end-h3|
.. sourcecode:: python
@mock_sesv2
def test_sesv2_behaviour:
boto3.client("sesv2")
...
|start-h3| Implemented features for this service |end-h3|
- [ ] batch_get_metric_data
- [ ] create_configuration_set
- [ ] create_configuration_set_event_destination
- [X] create_contact
- [X] create_contact_list
- [ ] create_custom_verification_email_template
- [ ] create_dedicated_ip_pool
- [ ] create_deliverability_test_report
- [ ] create_email_identity
- [ ] create_email_identity_policy
- [ ] create_email_template
- [ ] create_import_job
- [ ] delete_configuration_set
- [ ] delete_configuration_set_event_destination
- [X] delete_contact
- [X] delete_contact_list
- [ ] delete_custom_verification_email_template
- [ ] delete_dedicated_ip_pool
- [ ] delete_email_identity
- [ ] delete_email_identity_policy
- [ ] delete_email_template
- [ ] delete_suppressed_destination
- [ ] get_account
- [ ] get_blacklist_reports
- [ ] get_configuration_set
- [ ] get_configuration_set_event_destinations
- [X] get_contact
- [X] get_contact_list
- [ ] get_custom_verification_email_template
- [ ] get_dedicated_ip
- [ ] get_dedicated_ip_pool
- [ ] get_dedicated_ips
- [ ] get_deliverability_dashboard_options
- [ ] get_deliverability_test_report
- [ ] get_domain_deliverability_campaign
- [ ] get_domain_statistics_report
- [ ] get_email_identity
- [ ] get_email_identity_policies
- [ ] get_email_template
- [ ] get_import_job
- [ ] get_suppressed_destination
- [ ] list_configuration_sets
- [X] list_contact_lists
- [X] list_contacts
- [ ] list_custom_verification_email_templates
- [ ] list_dedicated_ip_pools
- [ ] list_deliverability_test_reports
- [ ] list_domain_deliverability_campaigns
- [ ] list_email_identities
- [ ] list_email_templates
- [ ] list_import_jobs
- [ ] list_recommendations
- [ ] list_suppressed_destinations
- [ ] list_tags_for_resource
- [ ] put_account_dedicated_ip_warmup_attributes
- [ ] put_account_details
- [ ] put_account_sending_attributes
- [ ] put_account_suppression_attributes
- [ ] put_account_vdm_attributes
- [ ] put_configuration_set_delivery_options
- [ ] put_configuration_set_reputation_options
- [ ] put_configuration_set_sending_options
- [ ] put_configuration_set_suppression_options
- [ ] put_configuration_set_tracking_options
- [ ] put_configuration_set_vdm_options
- [ ] put_dedicated_ip_in_pool
- [ ] put_dedicated_ip_warmup_attributes
- [ ] put_deliverability_dashboard_option
- [ ] put_email_identity_configuration_set_attributes
- [ ] put_email_identity_dkim_attributes
- [ ] put_email_identity_dkim_signing_attributes
- [ ] put_email_identity_feedback_attributes
- [ ] put_email_identity_mail_from_attributes
- [ ] put_suppressed_destination
- [ ] send_bulk_email
- [ ] send_custom_verification_email
- [X] send_email
- [ ] tag_resource
- [ ] test_render_email_template
- [ ] untag_resource
- [ ] update_configuration_set_event_destination
- [ ] update_contact
- [ ] update_contact_list
- [ ] update_custom_verification_email_template
- [ ] update_email_identity_policy
- [ ] update_email_template

View File

@ -109,10 +109,6 @@ ssm
- [ ] get_automation_execution
- [ ] get_calendar_state
- [X] get_command_invocation
https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_GetCommandInvocation.html
- [ ] get_connection_status
- [ ] get_default_patch_baseline
- [ ] get_deployable_patch_snapshot_for_instance
@ -148,7 +144,7 @@ ssm
- [ ] list_command_invocations
- [X] list_commands
https://docs.aws.amazon.com/systems-manager/latest/APIReference/API_ListCommands.html
Pagination and the Filters-parameter is not yet implemented
- [ ] list_compliance_items

View File

@ -150,6 +150,7 @@ mock_servicequotas = lazy_load(
".servicequotas", "mock_servicequotas", boto3_name="service-quotas"
)
mock_ses = lazy_load(".ses", "mock_ses")
mock_sesv2 = lazy_load(".sesv2", "mock_sesv2")
mock_servicediscovery = lazy_load(".servicediscovery", "mock_servicediscovery")
mock_signer = lazy_load(".signer", "mock_signer")
mock_sns = lazy_load(".sns", "mock_sns")

View File

@ -158,6 +158,8 @@ backend_url_patterns = [
("service-quotas", re.compile("https?://servicequotas\\.(.+)\\.amazonaws\\.com")),
("ses", re.compile("https?://email\\.(.+)\\.amazonaws\\.com")),
("ses", re.compile("https?://ses\\.(.+)\\.amazonaws\\.com")),
("sesv2", re.compile("https?://ses\\.(.+)\\.amazonaws\\.com")),
("sesv2", re.compile("https?://email\\.(.+)\\.amazonaws\\.com")),
("signer", re.compile("https?://signer\\.(.+)\\.amazonaws\\.com")),
("sns", re.compile("https?://sns\\.(.+)\\.amazonaws\\.com")),
("sqs", re.compile("https?://(.*\\.)?(queue|sqs)\\.(.*\\.)?amazonaws\\.com")),

View File

@ -153,6 +153,8 @@ class DomainDispatcherApplication:
path.startswith("/v20180820/") or "s3-control" in environ["HTTP_HOST"]
):
host = "s3control"
elif service == "ses" and path.startswith("/v2/"):
host = "sesv2"
else:
host = f"{service}.{region}.amazonaws.com"

5
moto/sesv2/__init__.py Normal file
View File

@ -0,0 +1,5 @@
"""sesv2 module initialization; sets value for base decorator."""
from .models import sesv2_backends
from ..core.models import base_decorator
mock_sesv2 = base_decorator(sesv2_backends)

8
moto/sesv2/exceptions.py Normal file
View File

@ -0,0 +1,8 @@
from moto.core.exceptions import JsonRESTError
class NotFoundException(JsonRESTError):
code = 404
def __init__(self, message: str):
super().__init__("NotFoundException", message)

166
moto/sesv2/models.py Normal file
View File

@ -0,0 +1,166 @@
"""SESV2Backend class with methods for supported APIs."""
from datetime import datetime as dt
from moto.core import BackendDict, BaseBackend, BaseModel
from ..ses.models import ses_backends, Message, RawMessage
from typing import Dict, List, Any
from .exceptions import NotFoundException
from moto.core.utils import iso_8601_datetime_with_milliseconds
class Contact(BaseModel):
def __init__(
self,
contact_list_name: str,
email_address: str,
topic_preferences: List[Dict[str, str]],
unsubscribe_all: bool,
) -> None:
self.contact_list_name = contact_list_name
self.email_address = email_address
self.topic_default_preferences: List[Dict[str, str]] = []
self.topic_preferences = topic_preferences
self.unsubscribe_all = unsubscribe_all
self.created_timestamp = iso_8601_datetime_with_milliseconds(dt.utcnow())
self.last_updated_timestamp = iso_8601_datetime_with_milliseconds(dt.utcnow())
@property
def response_object(self) -> Dict[str, Any]: # type: ignore[misc]
return {
"ContactListName": self.contact_list_name,
"EmailAddress": self.email_address,
"TopicDefaultPreferences": self.topic_default_preferences,
"TopicPreferences": self.topic_preferences,
"UnsubscribeAll": self.unsubscribe_all,
"CreatedTimestamp": self.created_timestamp,
"LastUpdatedTimestamp": self.last_updated_timestamp,
}
class ContactList(BaseModel):
def __init__(
self,
contact_list_name: str,
description: str,
topics: List[Dict[str, str]],
) -> None:
self.contact_list_name = contact_list_name
self.description = description
self.topics = topics
self.created_timestamp = iso_8601_datetime_with_milliseconds(dt.utcnow())
self.last_updated_timestamp = iso_8601_datetime_with_milliseconds(dt.utcnow())
self.contacts: Dict[str, Contact] = {}
def create_contact(self, contact_list_name: str, params: Dict[str, Any]) -> None:
email_address = params["EmailAddress"]
topic_preferences = (
[] if "TopicPreferences" not in params else params["TopicPreferences"]
)
unsubscribe_all = (
False if "UnsubscribeAll" not in params else params["UnsubscribeAll"]
)
new_contact = Contact(
contact_list_name, email_address, topic_preferences, unsubscribe_all
)
self.contacts[email_address] = new_contact
def list_contacts(self) -> List[Contact]:
return self.contacts.values() # type: ignore[return-value]
def get_contact(self, email: str) -> Contact:
if email in self.contacts:
return self.contacts[email]
else:
raise NotFoundException(f"{email} doesn't exist in List.")
def delete_contact(self, email: str) -> None:
# delete if contact exists, otherwise get_contact will throw appropriate exception
if self.get_contact(email):
del self.contacts[email]
@property
def response_object(self) -> Dict[str, Any]: # type: ignore[misc]
return {
"ContactListName": self.contact_list_name,
"Description": self.description,
"Topics": self.topics,
"CreatedTimestamp": self.created_timestamp,
"LastUpdatedTimestamp": self.last_updated_timestamp,
}
class SESV2Backend(BaseBackend):
"""Implementation of SESV2 APIs, piggy back on v1 SES"""
def __init__(self, region_name: str, account_id: str):
super().__init__(region_name, account_id)
self.contacts: Dict[str, Contact] = {}
self.contacts_lists: Dict[str, ContactList] = {}
def create_contact_list(self, params: Dict[str, Any]) -> None:
name = params["ContactListName"]
description = params.get("Description")
topics = [] if "Topics" not in params else params["Topics"]
new_list = ContactList(name, str(description), topics)
self.contacts_lists[name] = new_list
def get_contact_list(self, contact_list_name: str) -> ContactList:
if contact_list_name in self.contacts_lists:
return self.contacts_lists[contact_list_name]
else:
raise NotFoundException(
f"List with name: {contact_list_name} doesn't exist."
)
def list_contact_lists(self) -> List[ContactList]:
return self.contacts_lists.values() # type: ignore[return-value]
def delete_contact_list(self, name: str) -> None:
if name in self.contacts_lists:
del self.contacts_lists[name]
else:
raise NotFoundException(f"List with name: {name} doesn't exist")
def create_contact(self, contact_list_name: str, params: Dict[str, Any]) -> None:
contact_list = self.get_contact_list(contact_list_name)
contact_list.create_contact(contact_list_name, params)
return
def get_contact(self, email: str, contact_list_name: str) -> Contact:
contact_list = self.get_contact_list(contact_list_name)
contact = contact_list.get_contact(email)
return contact
def list_contacts(self, contact_list_name: str) -> List[Contact]:
contact_list = self.get_contact_list(contact_list_name)
contacts = contact_list.list_contacts()
return contacts
def delete_contact(self, email: str, contact_list_name: str) -> None:
contact_list = self.get_contact_list(contact_list_name)
contact_list.delete_contact(email)
return
def send_email(
self, source: str, destinations: Dict[str, List[str]], subject: str, body: str
) -> Message:
v1_backend = ses_backends[self.account_id][self.region_name]
message = v1_backend.send_email(
source=source,
destinations=destinations,
subject=subject,
body=body,
)
return message
def send_raw_email(
self, source: str, destinations: List[str], raw_data: str
) -> RawMessage:
v1_backend = ses_backends[self.account_id][self.region_name]
message = v1_backend.send_raw_email(
source=source, destinations=destinations, raw_data=raw_data
)
return message
sesv2_backends = BackendDict(SESV2Backend, "sesv2")

102
moto/sesv2/responses.py Normal file
View File

@ -0,0 +1,102 @@
"""Handles incoming sesv2 requests, invokes methods, returns responses."""
import json
from moto.core.responses import BaseResponse
from .models import sesv2_backends
from ..ses.responses import SEND_EMAIL_RESPONSE
from .models import SESV2Backend
from typing import List, Dict, Any
from urllib.parse import unquote
class SESV2Response(BaseResponse):
"""Handler for SESV2 requests and responses."""
def __init__(self) -> None:
super().__init__(service_name="sesv2")
@property
def sesv2_backend(self) -> SESV2Backend:
"""Return backend instance specific for this region."""
return sesv2_backends[self.current_account][self.region]
def send_email(self) -> str:
"""Piggy back on functionality from v1 mostly"""
params = get_params_dict(self.querystring)
from_email_address = params.get("FromEmailAddress")
destination = params.get("Destination")
content = params.get("Content")
if "Raw" in content:
all_destinations: List[str] = []
if "ToAddresses" in destination:
all_destinations = all_destinations + destination["ToAddresses"]
if "CcAddresses" in destination:
all_destinations = all_destinations + destination["CcAddresses"]
if "BccAddresses" in destination:
all_destinations = all_destinations + destination["BccAddresses"]
message = self.sesv2_backend.send_raw_email(
source=from_email_address,
destinations=all_destinations,
raw_data=content["Raw"]["Data"],
)
elif "Simple" in content:
message = self.sesv2_backend.send_email( # type: ignore
source=from_email_address,
destinations=destination,
subject=content["Simple"]["Subject"]["Data"],
body=content["Simple"]["Subject"]["Data"],
)
elif "Template" in content:
raise NotImplementedError("Template functionality not ready")
# use v1 templates as response same in v1 and v2
template = self.response_template(SEND_EMAIL_RESPONSE)
return template.render(message=message)
def create_contact_list(self) -> str:
params = get_params_dict(self.data)
self.sesv2_backend.create_contact_list(params)
return json.dumps({})
def get_contact_list(self) -> str:
contact_list_name = self._get_param("ContactListName")
contact_list = self.sesv2_backend.get_contact_list(contact_list_name)
return json.dumps(contact_list.response_object)
def list_contact_lists(self) -> str:
contact_lists = self.sesv2_backend.list_contact_lists()
return json.dumps(dict(ContactLists=[c.response_object for c in contact_lists]))
def delete_contact_list(self) -> str:
name = self._get_param("ContactListName")
self.sesv2_backend.delete_contact_list(name)
return json.dumps({})
def create_contact(self) -> str:
contact_list_name = self._get_param("ContactListName")
params = get_params_dict(self.data)
self.sesv2_backend.create_contact(contact_list_name, params)
return json.dumps({})
def get_contact(self) -> str:
email = unquote(self._get_param("EmailAddress"))
contact_list_name = self._get_param("ContactListName")
contact = self.sesv2_backend.get_contact(email, contact_list_name)
return json.dumps(contact.response_object)
def list_contacts(self) -> str:
contact_list_name = self._get_param("ContactListName")
contacts = self.sesv2_backend.list_contacts(contact_list_name)
return json.dumps(dict(Contacts=[c.response_object for c in contacts]))
def delete_contact(self) -> str:
email = self._get_param("EmailAddress")
contact_list_name = self._get_param("ContactListName")
self.sesv2_backend.delete_contact(unquote(email), contact_list_name)
return json.dumps({})
def get_params_dict(odict: Dict[str, Any]) -> Any:
# parsing of these params is nasty, hopefully there is a tidier way
return json.loads(list(dict(odict.items()).keys())[0])

19
moto/sesv2/urls.py Normal file
View File

@ -0,0 +1,19 @@
"""sesv2 base URL and path."""
from .responses import SESV2Response
url_bases = [
r"https?://email\.(.+)\.amazonaws\.com",
]
response = SESV2Response()
url_paths = {
"{0}/v2/email/outbound-emails$": response.dispatch,
"{0}/v2/email/contact-lists/(?P<name>[^/]+)$": response.dispatch,
"{0}/v2/email/contact-lists/(?P<name>[^/]+)/contacts$": response.dispatch,
"{0}/v2/email/contact-lists/(?P<name>[^/]+)/contacts/(?P<email>[^/]+)$": response.dispatch,
"{0}/v2/email/contact-lists$": response.dispatch,
"{0}/v2/.*$": response.dispatch,
}

View File

@ -61,6 +61,7 @@ def calculate_extended_implementation_coverage():
operation_names = [
xform_name(op) for op in real_client.meta.service_model.operation_names
]
for op in operation_names:
if moto_client and op in dir(moto_client):
implemented[op] = getattr(moto_client, op)

View File

View File

@ -0,0 +1,13 @@
"""Test different server responses."""
import moto.server as server
def test_sesv2_list():
backend = server.create_backend_app("sesv2")
test_client = backend.test_client()
resp = test_client.get("/v2/email/contact-lists")
assert resp.status_code == 200
assert resp.data == b'{"ContactLists": []}'

View File

@ -0,0 +1,323 @@
import boto3
from botocore.exceptions import ClientError
import pytest
from moto import mock_sesv2, mock_ses
from ..test_ses.test_ses_boto3 import get_raw_email
@pytest.fixture(scope="function")
def ses_v1():
"""Use this for API calls which exist in v1 but not in v2"""
with mock_ses():
yield boto3.client("ses", region_name="us-east-1")
@mock_sesv2
def test_send_email(ses_v1): # pylint: disable=redefined-outer-name
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
kwargs = dict(
FromEmailAddress="test@example.com",
Destination={
"ToAddresses": ["test_to@example.com"],
"CcAddresses": ["test_cc@example.com"],
"BccAddresses": ["test_bcc@example.com"],
},
Content={
"Simple": {
"Subject": {"Data": "test subject"},
"Body": {"Text": {"Data": "test body"}},
},
},
)
# Execute
with pytest.raises(ClientError) as e:
conn.send_email(**kwargs)
assert e.value.response["Error"]["Code"] == "MessageRejected"
ses_v1.verify_domain_identity(Domain="example.com")
conn.send_email(**kwargs)
send_quota = ses_v1.get_send_quota()
# Verify
sent_count = int(send_quota["SentLast24Hours"])
assert sent_count == 3
@mock_sesv2
def test_send_raw_email(ses_v1): # pylint: disable=redefined-outer-name
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
message = get_raw_email()
destination = {
"ToAddresses": [x.strip() for x in message["To"].split(",")],
}
kwargs = dict(
FromEmailAddress=message["From"],
Destination=destination,
Content={"Raw": {"Data": message.as_bytes()}},
)
# Execute
ses_v1.verify_email_identity(EmailAddress="test@example.com")
conn.send_email(**kwargs)
# Verify
send_quota = ses_v1.get_send_quota()
sent_count = int(send_quota["SentLast24Hours"])
assert sent_count == 2
@mock_sesv2
def test_create_contact_list():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
# Execute
conn.create_contact_list(
ContactListName=contact_list_name,
)
result = conn.list_contact_lists()
# Verify
assert len(result["ContactLists"]) == 1
assert result["ContactLists"][0]["ContactListName"] == contact_list_name
@mock_sesv2
def test_list_contact_lists():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
# Execute
result = conn.list_contact_lists()
# Verify
assert result["ContactLists"] == []
@mock_sesv2
def test_get_contact_list():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
# Execute
with pytest.raises(ClientError) as e:
conn.get_contact_list(ContactListName=contact_list_name)
assert e.value.response["Error"]["Code"] == "NotFoundException"
assert (
e.value.response["Error"]["Message"]
== f"List with name: {contact_list_name} doesn't exist."
)
conn.create_contact_list(
ContactListName=contact_list_name,
)
result = conn.get_contact_list(ContactListName=contact_list_name)
# Verify
assert result["ContactListName"] == contact_list_name
@mock_sesv2
def test_delete_contact_list():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
# Execute
with pytest.raises(ClientError) as e:
conn.delete_contact_list(ContactListName=contact_list_name)
assert e.value.response["Error"]["Code"] == "NotFoundException"
conn.create_contact_list(
ContactListName=contact_list_name,
)
result = conn.list_contact_lists()
assert len(result["ContactLists"]) == 1
conn.delete_contact_list(
ContactListName=contact_list_name,
)
result = conn.list_contact_lists()
# Verify
assert len(result["ContactLists"]) == 0
@mock_sesv2
def test_list_contacts():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
conn.create_contact_list(
ContactListName=contact_list_name,
)
# Execute
result = conn.list_contacts(ContactListName=contact_list_name)
# Verify
assert result["Contacts"] == []
@mock_sesv2
def test_create_contact_no_contact_list():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
email = "test@example.com"
# Execute
with pytest.raises(ClientError) as e:
conn.create_contact(
ContactListName=contact_list_name,
EmailAddress=email,
)
# Verify
assert e.value.response["Error"]["Code"] == "NotFoundException"
assert (
e.value.response["Error"]["Message"]
== f"List with name: {contact_list_name} doesn't exist."
)
@mock_sesv2
def test_create_contact():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
email = "test@example.com"
conn.create_contact_list(
ContactListName=contact_list_name,
)
# Execute
conn.create_contact(
ContactListName=contact_list_name,
EmailAddress=email,
)
result = conn.list_contacts(ContactListName=contact_list_name)
# Verify
assert len(result["Contacts"]) == 1
assert result["Contacts"][0]["EmailAddress"] == email
@mock_sesv2
def test_get_contact_no_contact_list():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
email = "test@example.com"
# Execute
with pytest.raises(ClientError) as e:
conn.get_contact(ContactListName=contact_list_name, EmailAddress=email)
# Verify
assert e.value.response["Error"]["Code"] == "NotFoundException"
assert (
e.value.response["Error"]["Message"]
== f"List with name: {contact_list_name} doesn't exist."
)
@mock_sesv2
def test_get_contact():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
email = "test@example.com"
conn.create_contact_list(
ContactListName=contact_list_name,
)
# Execute
conn.create_contact(
ContactListName=contact_list_name,
EmailAddress=email,
)
result = conn.get_contact(ContactListName=contact_list_name, EmailAddress=email)
# Verify
assert result["ContactListName"] == contact_list_name
assert result["EmailAddress"] == email
@mock_sesv2
def test_get_contact_no_contact():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
email = "test@example.com"
conn.create_contact_list(
ContactListName=contact_list_name,
)
# Execute
with pytest.raises(ClientError) as e:
conn.get_contact(ContactListName=contact_list_name, EmailAddress=email)
# Verify
assert e.value.response["Error"]["Code"] == "NotFoundException"
assert e.value.response["Error"]["Message"] == f"{email} doesn't exist in List."
@mock_sesv2
def test_delete_contact_no_contact_list():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
email = "test@example.com"
# Execute
with pytest.raises(ClientError) as e:
conn.delete_contact(ContactListName=contact_list_name, EmailAddress=email)
# Verify
assert e.value.response["Error"]["Code"] == "NotFoundException"
assert (
e.value.response["Error"]["Message"]
== f"List with name: {contact_list_name} doesn't exist."
)
@mock_sesv2
def test_delete_contact_no_contact():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
email = "test@example.com"
# Execute
conn.create_contact_list(ContactListName=contact_list_name)
with pytest.raises(ClientError) as e:
conn.delete_contact(ContactListName=contact_list_name, EmailAddress=email)
# Verify
assert e.value.response["Error"]["Code"] == "NotFoundException"
assert e.value.response["Error"]["Message"] == f"{email} doesn't exist in List."
@mock_sesv2
def test_delete_contact():
# Setup
conn = boto3.client("sesv2", region_name="us-east-1")
contact_list_name = "test2"
email = "test@example.com"
# Execute
conn.create_contact_list(ContactListName=contact_list_name)
conn.create_contact(ContactListName=contact_list_name, EmailAddress=email)
result = conn.list_contacts(ContactListName=contact_list_name)
assert len(result["Contacts"]) == 1
conn.delete_contact(ContactListName=contact_list_name, EmailAddress=email)
result = conn.list_contacts(ContactListName=contact_list_name)
# Verify
assert len(result["Contacts"]) == 0