Techdebt: MyPy SES (#6252)

This commit is contained in:
Bert Blommers 2023-04-25 11:42:08 +00:00 committed by GitHub
parent fe7b15c9f9
commit 65611082be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 261 additions and 198 deletions

View File

@ -4,98 +4,98 @@ from moto.core.exceptions import RESTError
class MessageRejectedError(RESTError): class MessageRejectedError(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("MessageRejected", message) super().__init__("MessageRejected", message)
class ConfigurationSetDoesNotExist(RESTError): class ConfigurationSetDoesNotExist(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("ConfigurationSetDoesNotExist", message) super().__init__("ConfigurationSetDoesNotExist", message)
class ConfigurationSetAlreadyExists(RESTError): class ConfigurationSetAlreadyExists(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("ConfigurationSetAlreadyExists", message) super().__init__("ConfigurationSetAlreadyExists", message)
class EventDestinationAlreadyExists(RESTError): class EventDestinationAlreadyExists(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("EventDestinationAlreadyExists", message) super().__init__("EventDestinationAlreadyExists", message)
class TemplateNameAlreadyExists(RESTError): class TemplateNameAlreadyExists(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("TemplateNameAlreadyExists", message) super().__init__("TemplateNameAlreadyExists", message)
class ValidationError(RESTError): class ValidationError(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("ValidationError", message) super().__init__("ValidationError", message)
class InvalidParameterValue(RESTError): class InvalidParameterValue(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("InvalidParameterValue", message) super().__init__("InvalidParameterValue", message)
class InvalidRenderingParameterException: class InvalidRenderingParameterException(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("InvalidRenderingParameterException", message) super().__init__("InvalidRenderingParameterException", message)
class TemplateDoesNotExist(RESTError): class TemplateDoesNotExist(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("TemplateDoesNotExist", message) super().__init__("TemplateDoesNotExist", message)
class RuleSetNameAlreadyExists(RESTError): class RuleSetNameAlreadyExists(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("RuleSetNameAlreadyExists", message) super().__init__("RuleSetNameAlreadyExists", message)
class RuleAlreadyExists(RESTError): class RuleAlreadyExists(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("RuleAlreadyExists", message) super().__init__("RuleAlreadyExists", message)
class RuleSetDoesNotExist(RESTError): class RuleSetDoesNotExist(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("RuleSetDoesNotExist", message) super().__init__("RuleSetDoesNotExist", message)
class RuleDoesNotExist(RESTError): class RuleDoesNotExist(RESTError):
code = 400 code = 400
def __init__(self, message): def __init__(self, message: str):
super().__init__("RuleDoesNotExist", message) super().__init__("RuleDoesNotExist", message)
class MissingRenderingAttributeException(RESTError): class MissingRenderingAttributeException(RESTError):
code = 400 code = 400
def __init__(self, var): def __init__(self, var: str):
super().__init__( super().__init__(
"MissingRenderingAttributeException", "MissingRenderingAttributeException",
f"Attribute '{var}' is not present in the rendering data.", f"Attribute '{var}' is not present in the rendering data.",

View File

@ -5,7 +5,7 @@ from email.mime.base import MIMEBase
from email.utils import parseaddr from email.utils import parseaddr
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.encoders import encode_7or8bit from email.encoders import encode_7or8bit
from typing import Mapping from typing import Any, Dict, List, Optional
from moto.core import BaseBackend, BackendDict, BaseModel from moto.core import BaseBackend, BackendDict, BaseModel
from moto.sns.models import sns_backends from moto.sns.models import sns_backends
@ -48,8 +48,8 @@ class SESFeedback(BaseModel):
FORWARDING_ENABLED = "feedback_forwarding_enabled" FORWARDING_ENABLED = "feedback_forwarding_enabled"
@staticmethod @staticmethod
def generate_message(account_id, msg_type): def generate_message(account_id: str, msg_type: str) -> Dict[str, Any]: # type: ignore[misc]
msg = dict(COMMON_MAIL) msg: Dict[str, Any] = dict(COMMON_MAIL)
msg["mail"]["sendingAccountId"] = account_id msg["mail"]["sendingAccountId"] = account_id
if msg_type == SESFeedback.BOUNCE: if msg_type == SESFeedback.BOUNCE:
msg["bounce"] = BOUNCE msg["bounce"] = BOUNCE
@ -62,7 +62,14 @@ class SESFeedback(BaseModel):
class Message(BaseModel): class Message(BaseModel):
def __init__(self, message_id, source, subject, body, destinations): def __init__(
self,
message_id: str,
source: str,
subject: str,
body: str,
destinations: Dict[str, List[str]],
):
self.id = message_id self.id = message_id
self.source = source self.source = source
self.subject = subject self.subject = subject
@ -71,7 +78,14 @@ class Message(BaseModel):
class TemplateMessage(BaseModel): class TemplateMessage(BaseModel):
def __init__(self, message_id, source, template, template_data, destinations): def __init__(
self,
message_id: str,
source: str,
template: List[str],
template_data: List[str],
destinations: Any,
):
self.id = message_id self.id = message_id
self.source = source self.source = source
self.template = template self.template = template
@ -80,7 +94,14 @@ class TemplateMessage(BaseModel):
class BulkTemplateMessage(BaseModel): class BulkTemplateMessage(BaseModel):
def __init__(self, message_ids, source, template, template_data, destinations): def __init__(
self,
message_ids: List[str],
source: str,
template: List[str],
template_data: List[str],
destinations: Any,
):
self.ids = message_ids self.ids = message_ids
self.source = source self.source = source
self.template = template self.template = template
@ -89,7 +110,9 @@ class BulkTemplateMessage(BaseModel):
class RawMessage(BaseModel): class RawMessage(BaseModel):
def __init__(self, message_id, source, destinations, raw_data): def __init__(
self, message_id: str, source: str, destinations: List[str], raw_data: str
):
self.id = message_id self.id = message_id
self.source = source self.source = source
self.destinations = destinations self.destinations = destinations
@ -97,11 +120,11 @@ class RawMessage(BaseModel):
class SESQuota(BaseModel): class SESQuota(BaseModel):
def __init__(self, sent): def __init__(self, sent: int):
self.sent = sent self.sent = sent
@property @property
def sent_past_24(self): def sent_past_24(self) -> int:
return self.sent return self.sent
@ -121,23 +144,23 @@ class SESBackend(BaseBackend):
Note that, as this is an internal API, the exact format may differ per versions. Note that, as this is an internal API, the exact format may differ per versions.
""" """
def __init__(self, region_name, account_id): def __init__(self, region_name: str, account_id: str):
super().__init__(region_name, account_id) super().__init__(region_name, account_id)
self.addresses = [] self.addresses: List[str] = []
self.email_addresses = [] self.email_addresses: List[str] = []
self.domains = [] self.domains: List[str] = []
self.sent_messages = [] self.sent_messages: List[Any] = []
self.sent_message_count = 0 self.sent_message_count = 0
self.rejected_messages_count = 0 self.rejected_messages_count = 0
self.sns_topics = {} self.sns_topics: Dict[str, Dict[str, Any]] = {}
self.config_set = {} self.config_set: Dict[str, int] = {}
self.config_set_event_destination = {} self.config_set_event_destination: Dict[str, Dict[str, Any]] = {}
self.event_destinations = {} self.event_destinations: Dict[str, int] = {}
self.identity_mail_from_domains = {} self.identity_mail_from_domains: Dict[str, Dict[str, Any]] = {}
self.templates = {} self.templates: Dict[str, Dict[str, str]] = {}
self.receipt_rule_set = {} self.receipt_rule_set: Dict[str, List[Dict[str, Any]]] = {}
def _is_verified_address(self, source): def _is_verified_address(self, source: str) -> bool:
_, address = parseaddr(source) _, address = parseaddr(source)
if address in self.addresses: if address in self.addresses:
return True return True
@ -146,32 +169,34 @@ class SESBackend(BaseBackend):
_, host = address.split("@", 1) _, host = address.split("@", 1)
return host in self.domains return host in self.domains
def verify_email_identity(self, address): def verify_email_identity(self, address: str) -> None:
_, address = parseaddr(address) _, address = parseaddr(address)
if address not in self.addresses: if address not in self.addresses:
self.addresses.append(address) self.addresses.append(address)
def verify_email_address(self, address): def verify_email_address(self, address: str) -> None:
_, address = parseaddr(address) _, address = parseaddr(address)
self.email_addresses.append(address) self.email_addresses.append(address)
def verify_domain(self, domain): def verify_domain(self, domain: str) -> None:
if domain.lower() not in self.domains: if domain.lower() not in self.domains:
self.domains.append(domain.lower()) self.domains.append(domain.lower())
def list_identities(self): def list_identities(self) -> List[str]:
return self.domains + self.addresses return self.domains + self.addresses
def list_verified_email_addresses(self): def list_verified_email_addresses(self) -> List[str]:
return self.email_addresses return self.email_addresses
def delete_identity(self, identity): def delete_identity(self, identity: str) -> None:
if "@" in identity: if "@" in identity:
self.addresses.remove(identity) self.addresses.remove(identity)
else: else:
self.domains.remove(identity) self.domains.remove(identity)
def send_email(self, source, subject, body, destinations, region): def send_email(
self, source: str, subject: str, body: str, destinations: Dict[str, List[str]]
) -> Message:
recipient_count = sum(map(len, destinations.values())) recipient_count = sum(map(len, destinations.values()))
if recipient_count > RECIPIENT_LIMIT: if recipient_count > RECIPIENT_LIMIT:
raise MessageRejectedError("Too many recipients.") raise MessageRejectedError("Too many recipients.")
@ -182,11 +207,11 @@ class SESBackend(BaseBackend):
address for addresses in destinations.values() for address in addresses address for addresses in destinations.values() for address in addresses
] ]
for address in [source, *destination_addresses]: for address in [source, *destination_addresses]:
valid, msg = is_valid_address(address) msg = is_valid_address(address)
if not valid: if msg is not None:
raise InvalidParameterValue(msg) raise InvalidParameterValue(msg)
self.__process_sns_feedback__(source, destinations, region) self.__process_sns_feedback__(source, destinations)
message_id = get_random_message_id() message_id = get_random_message_id()
message = Message(message_id, source, subject, body, destinations) message = Message(message_id, source, subject, body, destinations)
@ -195,14 +220,18 @@ class SESBackend(BaseBackend):
return message return message
def send_bulk_templated_email( def send_bulk_templated_email(
self, source, template, template_data, destinations, region self,
): source: str,
template: List[str],
template_data: List[str],
destinations: List[Dict[str, Dict[str, List[str]]]],
) -> BulkTemplateMessage:
recipient_count = len(destinations) recipient_count = len(destinations)
if recipient_count > RECIPIENT_LIMIT: if recipient_count > RECIPIENT_LIMIT:
raise MessageRejectedError("Too many destinations.") raise MessageRejectedError("Too many destinations.")
total_recipient_count = sum( total_recipient_count = sum(
map(lambda d: sum(map(len, d["Destination"].values())), destinations) map(lambda d: sum(map(len, d["Destination"].values())), destinations) # type: ignore
) )
if total_recipient_count > RECIPIENT_LIMIT: if total_recipient_count > RECIPIENT_LIMIT:
raise MessageRejectedError("Too many destinations.") raise MessageRejectedError("Too many destinations.")
@ -214,7 +243,7 @@ class SESBackend(BaseBackend):
if not self.templates.get(template[0]): if not self.templates.get(template[0]):
raise TemplateDoesNotExist(f"Template ({template[0]}) does not exist") raise TemplateDoesNotExist(f"Template ({template[0]}) does not exist")
self.__process_sns_feedback__(source, destinations, region) self.__process_sns_feedback__(source, destinations)
message_id = get_random_message_id() message_id = get_random_message_id()
message = TemplateMessage( message = TemplateMessage(
@ -227,8 +256,12 @@ class SESBackend(BaseBackend):
return BulkTemplateMessage(ids, source, template, template_data, destinations) return BulkTemplateMessage(ids, source, template, template_data, destinations)
def send_templated_email( def send_templated_email(
self, source, template, template_data, destinations, region self,
): source: str,
template: List[str],
template_data: List[str],
destinations: Dict[str, List[str]],
) -> TemplateMessage:
recipient_count = sum(map(len, destinations.values())) recipient_count = sum(map(len, destinations.values()))
if recipient_count > RECIPIENT_LIMIT: if recipient_count > RECIPIENT_LIMIT:
raise MessageRejectedError("Too many recipients.") raise MessageRejectedError("Too many recipients.")
@ -239,14 +272,14 @@ class SESBackend(BaseBackend):
address for addresses in destinations.values() for address in addresses address for addresses in destinations.values() for address in addresses
] ]
for address in [source, *destination_addresses]: for address in [source, *destination_addresses]:
valid, msg = is_valid_address(address) msg = is_valid_address(address)
if not valid: if msg is not None:
raise InvalidParameterValue(msg) raise InvalidParameterValue(msg)
if not self.templates.get(template[0]): if not self.templates.get(template[0]):
raise TemplateDoesNotExist(f"Template ({template[0]}) does not exist") raise TemplateDoesNotExist(f"Template ({template[0]}) does not exist")
self.__process_sns_feedback__(source, destinations, region) self.__process_sns_feedback__(source, destinations)
message_id = get_random_message_id() message_id = get_random_message_id()
message = TemplateMessage( message = TemplateMessage(
@ -256,7 +289,7 @@ class SESBackend(BaseBackend):
self.sent_message_count += recipient_count self.sent_message_count += recipient_count
return message return message
def __type_of_message__(self, destinations): def __type_of_message__(self, destinations: Any) -> Optional[str]:
"""Checks the destination for any special address that could indicate delivery, """Checks the destination for any special address that could indicate delivery,
complaint or bounce like in SES simulator""" complaint or bounce like in SES simulator"""
if isinstance(destinations, list): if isinstance(destinations, list):
@ -278,11 +311,11 @@ class SESBackend(BaseBackend):
return None return None
def __generate_feedback__(self, msg_type): def __generate_feedback__(self, msg_type: str) -> Dict[str, Any]:
"""Generates the SNS message for the feedback""" """Generates the SNS message for the feedback"""
return SESFeedback.generate_message(self.account_id, msg_type) return SESFeedback.generate_message(self.account_id, msg_type)
def __process_sns_feedback__(self, source, destinations, region): def __process_sns_feedback__(self, source: str, destinations: Any) -> None:
domain = str(source) domain = str(source)
if "@" in domain: if "@" in domain:
domain = domain.split("@")[1] domain = domain.split("@")[1]
@ -293,11 +326,13 @@ class SESBackend(BaseBackend):
if sns_topic is not None: if sns_topic is not None:
message = self.__generate_feedback__(msg_type) message = self.__generate_feedback__(msg_type)
if message: if message:
sns_backends[self.account_id][region].publish( sns_backends[self.account_id][self.region_name].publish(
message, arn=sns_topic message, arn=sns_topic
) )
def send_raw_email(self, source, destinations, raw_data, region): def send_raw_email(
self, source: str, destinations: List[str], raw_data: str
) -> RawMessage:
if source is not None: if source is not None:
_, source_email_address = parseaddr(source) _, source_email_address = parseaddr(source)
if not self._is_verified_address(source_email_address): if not self._is_verified_address(source_email_address):
@ -324,33 +359,39 @@ class SESBackend(BaseBackend):
if recipient_count > RECIPIENT_LIMIT: if recipient_count > RECIPIENT_LIMIT:
raise MessageRejectedError("Too many recipients.") raise MessageRejectedError("Too many recipients.")
for address in [addr for addr in [source, *destinations] if addr is not None]: for address in [addr for addr in [source, *destinations] if addr is not None]:
valid, msg = is_valid_address(address) msg = is_valid_address(address)
if not valid: if msg is not None:
raise InvalidParameterValue(msg) raise InvalidParameterValue(msg)
self.__process_sns_feedback__(source, destinations, region) self.__process_sns_feedback__(source, destinations)
self.sent_message_count += recipient_count self.sent_message_count += recipient_count
message_id = get_random_message_id() message_id = get_random_message_id()
message = RawMessage(message_id, source, destinations, raw_data) raw_message = RawMessage(message_id, source, destinations, raw_data)
self.sent_messages.append(message) self.sent_messages.append(raw_message)
return message return raw_message
def get_send_quota(self): def get_send_quota(self) -> SESQuota:
return SESQuota(self.sent_message_count) return SESQuota(self.sent_message_count)
def get_identity_notification_attributes(self, identities): def get_identity_notification_attributes(
response = {} self, identities: List[str]
) -> Dict[str, Dict[str, Any]]:
response: Dict[str, Dict[str, Any]] = {}
for identity in identities: for identity in identities:
response[identity] = self.sns_topics.get(identity, {}) response[identity] = self.sns_topics.get(identity, {})
return response return response
def set_identity_feedback_forwarding_enabled(self, identity, enabled): def set_identity_feedback_forwarding_enabled(
self, identity: str, enabled: bool
) -> None:
identity_sns_topics = self.sns_topics.get(identity, {}) identity_sns_topics = self.sns_topics.get(identity, {})
identity_sns_topics[SESFeedback.FORWARDING_ENABLED] = enabled identity_sns_topics[SESFeedback.FORWARDING_ENABLED] = enabled
self.sns_topics[identity] = identity_sns_topics self.sns_topics[identity] = identity_sns_topics
def set_identity_notification_topic(self, identity, notification_type, sns_topic): def set_identity_notification_topic(
self, identity: str, notification_type: str, sns_topic: Optional[str]
) -> None:
identity_sns_topics = self.sns_topics.get(identity, {}) identity_sns_topics = self.sns_topics.get(identity, {})
if sns_topic is None: if sns_topic is None:
del identity_sns_topics[notification_type] del identity_sns_topics[notification_type]
@ -359,26 +400,22 @@ class SESBackend(BaseBackend):
self.sns_topics[identity] = identity_sns_topics self.sns_topics[identity] = identity_sns_topics
return {} def create_configuration_set(self, configuration_set_name: str) -> None:
def create_configuration_set(self, configuration_set_name):
if configuration_set_name in self.config_set: if configuration_set_name in self.config_set:
raise ConfigurationSetAlreadyExists( raise ConfigurationSetAlreadyExists(
f"Configuration set <{configuration_set_name}> already exists" f"Configuration set <{configuration_set_name}> already exists"
) )
self.config_set[configuration_set_name] = 1 self.config_set[configuration_set_name] = 1
return {}
def describe_configuration_set(self, configuration_set_name): def describe_configuration_set(self, configuration_set_name: str) -> None:
if configuration_set_name not in self.config_set: if configuration_set_name not in self.config_set:
raise ConfigurationSetDoesNotExist( raise ConfigurationSetDoesNotExist(
f"Configuration set <{configuration_set_name}> does not exist" f"Configuration set <{configuration_set_name}> does not exist"
) )
return {}
def create_configuration_set_event_destination( def create_configuration_set_event_destination(
self, configuration_set_name, event_destination self, configuration_set_name: str, event_destination: Dict[str, Any]
): ) -> None:
if self.config_set.get(configuration_set_name) is None: if self.config_set.get(configuration_set_name) is None:
raise ConfigurationSetDoesNotExist("Invalid Configuration Set Name.") raise ConfigurationSetDoesNotExist("Invalid Configuration Set Name.")
@ -389,19 +426,16 @@ class SESBackend(BaseBackend):
self.config_set_event_destination[configuration_set_name] = event_destination self.config_set_event_destination[configuration_set_name] = event_destination
self.event_destinations[event_destination["Name"]] = 1 self.event_destinations[event_destination["Name"]] = 1
return {} def get_send_statistics(self) -> Dict[str, Any]:
return {
"DeliveryAttempts": self.sent_message_count,
"Rejects": self.rejected_messages_count,
"Complaints": 0,
"Bounces": 0,
"Timestamp": datetime.datetime.utcnow(),
}
def get_send_statistics(self): def add_template(self, template_info: Dict[str, str]) -> None:
statistics = {}
statistics["DeliveryAttempts"] = self.sent_message_count
statistics["Rejects"] = self.rejected_messages_count
statistics["Complaints"] = 0
statistics["Bounces"] = 0
statistics["Timestamp"] = datetime.datetime.utcnow()
return statistics
def add_template(self, template_info):
template_name = template_info["template_name"] template_name = template_info["template_name"]
if not template_name: if not template_name:
raise ValidationError( raise ValidationError(
@ -418,7 +452,7 @@ class SESBackend(BaseBackend):
raise InvalidParameterValue("The subject must be specified.") raise InvalidParameterValue("The subject must be specified.")
self.templates[template_name] = template_info self.templates[template_name] = template_info
def update_template(self, template_info): def update_template(self, template_info: Dict[str, str]) -> None:
template_name = template_info["template_name"] template_name = template_info["template_name"]
if not template_name: if not template_name:
raise ValidationError( raise ValidationError(
@ -435,15 +469,15 @@ class SESBackend(BaseBackend):
raise InvalidParameterValue("The subject must be specified.") raise InvalidParameterValue("The subject must be specified.")
self.templates[template_name] = template_info self.templates[template_name] = template_info
def get_template(self, template_name): def get_template(self, template_name: str) -> Dict[str, str]:
if not self.templates.get(template_name, None): if not self.templates.get(template_name, None):
raise TemplateDoesNotExist("Invalid Template Name.") raise TemplateDoesNotExist("Invalid Template Name.")
return self.templates[template_name] return self.templates[template_name]
def list_templates(self): def list_templates(self) -> List[Dict[str, str]]:
return list(self.templates.values()) return list(self.templates.values())
def render_template(self, render_data): def render_template(self, render_data: Dict[str, Any]) -> str:
template_name = render_data.get("name", "") template_name = render_data.get("name", "")
template = self.templates.get(template_name, None) template = self.templates.get(template_name, None)
if not template: if not template:
@ -451,7 +485,7 @@ class SESBackend(BaseBackend):
template_data = render_data.get("data") template_data = render_data.get("data")
try: try:
template_data = json.loads(template_data) template_data = json.loads(template_data) # type: ignore
except ValueError: except ValueError:
raise InvalidRenderingParameterException( raise InvalidRenderingParameterException(
"Template rendering data is invalid" "Template rendering data is invalid"
@ -484,12 +518,12 @@ class SESBackend(BaseBackend):
) )
return rendered_template return rendered_template
def create_receipt_rule_set(self, rule_set_name): def create_receipt_rule_set(self, rule_set_name: str) -> None:
if self.receipt_rule_set.get(rule_set_name) is not None: if self.receipt_rule_set.get(rule_set_name) is not None:
raise RuleSetNameAlreadyExists("Duplicate Receipt Rule Set Name.") raise RuleSetNameAlreadyExists("Duplicate Receipt Rule Set Name.")
self.receipt_rule_set[rule_set_name] = [] self.receipt_rule_set[rule_set_name] = []
def create_receipt_rule(self, rule_set_name, rule): def create_receipt_rule(self, rule_set_name: str, rule: Dict[str, Any]) -> None:
rule_set = self.receipt_rule_set.get(rule_set_name) rule_set = self.receipt_rule_set.get(rule_set_name)
if rule_set is None: if rule_set is None:
raise RuleSetDoesNotExist("Invalid Rule Set Name.") raise RuleSetDoesNotExist("Invalid Rule Set Name.")
@ -498,7 +532,7 @@ class SESBackend(BaseBackend):
rule_set.append(rule) rule_set.append(rule)
self.receipt_rule_set[rule_set_name] = rule_set self.receipt_rule_set[rule_set_name] = rule_set
def describe_receipt_rule_set(self, rule_set_name): def describe_receipt_rule_set(self, rule_set_name: str) -> List[Dict[str, Any]]:
rule_set = self.receipt_rule_set.get(rule_set_name) rule_set = self.receipt_rule_set.get(rule_set_name)
if rule_set is None: if rule_set is None:
@ -506,7 +540,9 @@ class SESBackend(BaseBackend):
return rule_set return rule_set
def describe_receipt_rule(self, rule_set_name, rule_name): def describe_receipt_rule(
self, rule_set_name: str, rule_name: str
) -> Dict[str, Any]:
rule_set = self.receipt_rule_set.get(rule_set_name) rule_set = self.receipt_rule_set.get(rule_set_name)
if rule_set is None: if rule_set is None:
@ -518,7 +554,7 @@ class SESBackend(BaseBackend):
raise RuleDoesNotExist("Invalid Rule Name.") raise RuleDoesNotExist("Invalid Rule Name.")
def update_receipt_rule(self, rule_set_name, rule): def update_receipt_rule(self, rule_set_name: str, rule: Dict[str, Any]) -> None:
rule_set = self.receipt_rule_set.get(rule_set_name) rule_set = self.receipt_rule_set.get(rule_set_name)
if rule_set is None: if rule_set is None:
@ -532,8 +568,11 @@ class SESBackend(BaseBackend):
raise RuleDoesNotExist(f"Rule does not exist: {rule['name']}") raise RuleDoesNotExist(f"Rule does not exist: {rule['name']}")
def set_identity_mail_from_domain( def set_identity_mail_from_domain(
self, identity, mail_from_domain=None, behavior_on_mx_failure=None self,
): identity: str,
mail_from_domain: Optional[str] = None,
behavior_on_mx_failure: Optional[str] = None,
) -> None:
if identity not in (self.domains + self.addresses): if identity not in (self.domains + self.addresses):
raise InvalidParameterValue(f"Identity '{identity}' does not exist.") raise InvalidParameterValue(f"Identity '{identity}' does not exist.")
@ -560,7 +599,9 @@ class SESBackend(BaseBackend):
"behavior_on_mx_failure": behavior_on_mx_failure, "behavior_on_mx_failure": behavior_on_mx_failure,
} }
def get_identity_mail_from_domain_attributes(self, identities=None): def get_identity_mail_from_domain_attributes(
self, identities: Optional[List[str]] = None
) -> Dict[str, Dict[str, str]]:
if identities is None: if identities is None:
identities = [] identities = []
@ -573,11 +614,13 @@ class SESBackend(BaseBackend):
return attributes_by_identity return attributes_by_identity
def get_identity_verification_attributes(self, identities=None): def get_identity_verification_attributes(
self, identities: Optional[List[str]] = None
) -> Dict[str, str]:
if identities is None: if identities is None:
identities = [] identities = []
attributes_by_identity = {} attributes_by_identity: Dict[str, str] = {}
for identity in identities: for identity in identities:
if identity in (self.domains + self.addresses): if identity in (self.domains + self.addresses):
attributes_by_identity[identity] = "Success" attributes_by_identity[identity] = "Success"
@ -585,4 +628,4 @@ class SESBackend(BaseBackend):
return attributes_by_identity return attributes_by_identity
ses_backends: Mapping[str, SESBackend] = BackendDict(SESBackend, "ses") ses_backends = BackendDict(SESBackend, "ses")

View File

@ -1,66 +1,71 @@
import base64 import base64
from typing import Any, Dict, List
from moto.core.responses import BaseResponse from moto.core.responses import BaseResponse
from .models import ses_backends from .models import ses_backends, SESBackend
from datetime import datetime from datetime import datetime
class EmailResponse(BaseResponse): class EmailResponse(BaseResponse):
def __init__(self): def __init__(self) -> None:
super().__init__(service_name="ses") super().__init__(service_name="ses")
@property @property
def backend(self): def backend(self) -> SESBackend:
return ses_backends[self.current_account][self.region] return ses_backends[self.current_account][self.region]
def verify_email_identity(self): def verify_email_identity(self) -> str:
address = self.querystring.get("EmailAddress")[0] address = self.querystring.get("EmailAddress")[0] # type: ignore
self.backend.verify_email_identity(address) self.backend.verify_email_identity(address)
template = self.response_template(VERIFY_EMAIL_IDENTITY) template = self.response_template(VERIFY_EMAIL_IDENTITY)
return template.render() return template.render()
def verify_email_address(self): def verify_email_address(self) -> str:
address = self.querystring.get("EmailAddress")[0] address = self.querystring.get("EmailAddress")[0] # type: ignore
self.backend.verify_email_address(address) self.backend.verify_email_address(address)
template = self.response_template(VERIFY_EMAIL_ADDRESS) template = self.response_template(VERIFY_EMAIL_ADDRESS)
return template.render() return template.render()
def list_identities(self): def list_identities(self) -> str:
identities = self.backend.list_identities() identities = self.backend.list_identities()
template = self.response_template(LIST_IDENTITIES_RESPONSE) template = self.response_template(LIST_IDENTITIES_RESPONSE)
return template.render(identities=identities) return template.render(identities=identities)
def list_verified_email_addresses(self): def list_verified_email_addresses(self) -> str:
email_addresses = self.backend.list_verified_email_addresses() email_addresses = self.backend.list_verified_email_addresses()
template = self.response_template(LIST_VERIFIED_EMAIL_RESPONSE) template = self.response_template(LIST_VERIFIED_EMAIL_RESPONSE)
return template.render(email_addresses=email_addresses) return template.render(email_addresses=email_addresses)
def verify_domain_dkim(self): def verify_domain_dkim(self) -> str:
domain = self.querystring.get("Domain")[0] domain = self.querystring.get("Domain")[0] # type: ignore
self.backend.verify_domain(domain) self.backend.verify_domain(domain)
template = self.response_template(VERIFY_DOMAIN_DKIM_RESPONSE) template = self.response_template(VERIFY_DOMAIN_DKIM_RESPONSE)
return template.render() return template.render()
def verify_domain_identity(self): def verify_domain_identity(self) -> str:
domain = self.querystring.get("Domain")[0] domain = self.querystring.get("Domain")[0] # type: ignore
self.backend.verify_domain(domain) self.backend.verify_domain(domain)
template = self.response_template(VERIFY_DOMAIN_IDENTITY_RESPONSE) template = self.response_template(VERIFY_DOMAIN_IDENTITY_RESPONSE)
return template.render() return template.render()
def delete_identity(self): def delete_identity(self) -> str:
domain = self.querystring.get("Identity")[0] domain = self.querystring.get("Identity")[0] # type: ignore
self.backend.delete_identity(domain) self.backend.delete_identity(domain)
template = self.response_template(DELETE_IDENTITY_RESPONSE) template = self.response_template(DELETE_IDENTITY_RESPONSE)
return template.render() return template.render()
def send_email(self): def send_email(self) -> str:
bodydatakey = "Message.Body.Text.Data" bodydatakey = "Message.Body.Text.Data"
if "Message.Body.Html.Data" in self.querystring: if "Message.Body.Html.Data" in self.querystring:
bodydatakey = "Message.Body.Html.Data" bodydatakey = "Message.Body.Html.Data"
body = self.querystring.get(bodydatakey)[0] body = self.querystring.get(bodydatakey)[0] # type: ignore
source = self.querystring.get("Source")[0] source = self.querystring.get("Source")[0] # type: ignore
subject = self.querystring.get("Message.Subject.Data")[0] subject = self.querystring.get("Message.Subject.Data")[0] # type: ignore
destinations = {"ToAddresses": [], "CcAddresses": [], "BccAddresses": []} destinations: Dict[str, List[str]] = {
"ToAddresses": [],
"CcAddresses": [],
"BccAddresses": [],
}
for dest_type in destinations: for dest_type in destinations:
# consume up to 51 to allow exception # consume up to 51 to allow exception
for i in range(1, 52): for i in range(1, 52):
@ -70,18 +75,20 @@ class EmailResponse(BaseResponse):
break break
destinations[dest_type].append(address[0]) destinations[dest_type].append(address[0])
message = self.backend.send_email( message = self.backend.send_email(source, subject, body, destinations)
source, subject, body, destinations, self.region
)
template = self.response_template(SEND_EMAIL_RESPONSE) template = self.response_template(SEND_EMAIL_RESPONSE)
return template.render(message=message) return template.render(message=message)
def send_templated_email(self): def send_templated_email(self) -> str:
source = self.querystring.get("Source")[0] source = self.querystring.get("Source")[0] # type: ignore
template = self.querystring.get("Template") template: List[str] = self.querystring.get("Template") # type: ignore
template_data = self.querystring.get("TemplateData") template_data: List[str] = self.querystring.get("TemplateData") # type: ignore
destinations = {"ToAddresses": [], "CcAddresses": [], "BccAddresses": []} destinations: Dict[str, List[str]] = {
"ToAddresses": [],
"CcAddresses": [],
"BccAddresses": [],
}
for dest_type in destinations: for dest_type in destinations:
# consume up to 51 to allow exception # consume up to 51 to allow exception
for i in range(1, 52): for i in range(1, 52):
@ -92,13 +99,14 @@ class EmailResponse(BaseResponse):
destinations[dest_type].append(address[0]) destinations[dest_type].append(address[0])
message = self.backend.send_templated_email( message = self.backend.send_templated_email(
source, template, template_data, destinations, self.region source, template, template_data, destinations
)
return self.response_template(SEND_TEMPLATED_EMAIL_RESPONSE).render(
message=message
) )
template = self.response_template(SEND_TEMPLATED_EMAIL_RESPONSE)
return template.render(message=message)
def send_bulk_templated_email(self): def send_bulk_templated_email(self) -> str:
source = self.querystring.get("Source")[0] source = self.querystring.get("Source")[0] # type: ignore
template = self.querystring.get("Template") template = self.querystring.get("Template")
template_data = self.querystring.get("DefaultTemplateData") template_data = self.querystring.get("DefaultTemplateData")
@ -109,7 +117,11 @@ class EmailResponse(BaseResponse):
) )
if self.querystring.get(destination_field) is None: if self.querystring.get(destination_field) is None:
break break
destination = {"ToAddresses": [], "CcAddresses": [], "BccAddresses": []} destination: Dict[str, List[str]] = {
"ToAddresses": [],
"CcAddresses": [],
"BccAddresses": [],
}
for dest_type in destination: for dest_type in destination:
# consume up to 51 to allow exception # consume up to 51 to allow exception
for j in range(1, 52): for j in range(1, 52):
@ -123,18 +135,18 @@ class EmailResponse(BaseResponse):
destinations.append({"Destination": destination}) destinations.append({"Destination": destination})
message = self.backend.send_bulk_templated_email( message = self.backend.send_bulk_templated_email(
source, template, template_data, destinations, self.region source, template, template_data, destinations # type: ignore
) )
template = self.response_template(SEND_BULK_TEMPLATED_EMAIL_RESPONSE) template = self.response_template(SEND_BULK_TEMPLATED_EMAIL_RESPONSE)
result = template.render(message=message) result = template.render(message=message)
return result return result
def send_raw_email(self): def send_raw_email(self) -> str:
source = self.querystring.get("Source") source = self.querystring.get("Source")
if source is not None: if source is not None:
(source,) = source (source,) = source
raw_data = self.querystring.get("RawMessage.Data")[0] raw_data = self.querystring.get("RawMessage.Data")[0] # type: ignore
raw_data = base64.b64decode(raw_data) raw_data = base64.b64decode(raw_data)
raw_data = raw_data.decode("utf-8") raw_data = raw_data.decode("utf-8")
destinations = [] destinations = []
@ -146,35 +158,33 @@ class EmailResponse(BaseResponse):
break break
destinations.append(address[0]) destinations.append(address[0])
message = self.backend.send_raw_email( message = self.backend.send_raw_email(source, destinations, raw_data)
source, destinations, raw_data, self.region
)
template = self.response_template(SEND_RAW_EMAIL_RESPONSE) template = self.response_template(SEND_RAW_EMAIL_RESPONSE)
return template.render(message=message) return template.render(message=message)
def get_send_quota(self): def get_send_quota(self) -> str:
quota = self.backend.get_send_quota() quota = self.backend.get_send_quota()
template = self.response_template(GET_SEND_QUOTA_RESPONSE) template = self.response_template(GET_SEND_QUOTA_RESPONSE)
return template.render(quota=quota) return template.render(quota=quota)
def get_identity_notification_attributes(self): def get_identity_notification_attributes(self) -> str:
identities = self._get_params()["Identities"] identities = self._get_params()["Identities"]
identities = self.backend.get_identity_notification_attributes(identities) identities = self.backend.get_identity_notification_attributes(identities)
template = self.response_template(GET_IDENTITY_NOTIFICATION_ATTRIBUTES) template = self.response_template(GET_IDENTITY_NOTIFICATION_ATTRIBUTES)
return template.render(identities=identities) return template.render(identities=identities)
def set_identity_feedback_forwarding_enabled(self): def set_identity_feedback_forwarding_enabled(self) -> str:
identity = self._get_param("Identity") identity = self._get_param("Identity")
enabled = self._get_bool_param("ForwardingEnabled") enabled = self._get_bool_param("ForwardingEnabled")
self.backend.set_identity_feedback_forwarding_enabled(identity, enabled) self.backend.set_identity_feedback_forwarding_enabled(identity, enabled)
template = self.response_template(SET_IDENTITY_FORWARDING_ENABLED_RESPONSE) template = self.response_template(SET_IDENTITY_FORWARDING_ENABLED_RESPONSE)
return template.render() return template.render()
def set_identity_notification_topic(self): def set_identity_notification_topic(self) -> str:
identity = self.querystring.get("Identity")[0] identity = self.querystring.get("Identity")[0] # type: ignore
not_type = self.querystring.get("NotificationType")[0] not_type = self.querystring.get("NotificationType")[0] # type: ignore
sns_topic = self.querystring.get("SnsTopic") sns_topic = self.querystring.get("SnsTopic") # type: ignore
if sns_topic: if sns_topic:
sns_topic = sns_topic[0] sns_topic = sns_topic[0]
@ -182,33 +192,35 @@ class EmailResponse(BaseResponse):
template = self.response_template(SET_IDENTITY_NOTIFICATION_TOPIC_RESPONSE) template = self.response_template(SET_IDENTITY_NOTIFICATION_TOPIC_RESPONSE)
return template.render() return template.render()
def get_send_statistics(self): def get_send_statistics(self) -> str:
statistics = self.backend.get_send_statistics() statistics = self.backend.get_send_statistics()
template = self.response_template(GET_SEND_STATISTICS) template = self.response_template(GET_SEND_STATISTICS)
return template.render(all_statistics=[statistics]) return template.render(all_statistics=[statistics])
def create_configuration_set(self): def create_configuration_set(self) -> str:
configuration_set_name = self.querystring.get("ConfigurationSet.Name")[0] configuration_set_name = self.querystring.get("ConfigurationSet.Name")[0] # type: ignore
self.backend.create_configuration_set( self.backend.create_configuration_set(
configuration_set_name=configuration_set_name configuration_set_name=configuration_set_name
) )
template = self.response_template(CREATE_CONFIGURATION_SET) template = self.response_template(CREATE_CONFIGURATION_SET)
return template.render() return template.render()
def describe_configuration_set(self): def describe_configuration_set(self) -> str:
configuration_set_name = self.querystring.get("ConfigurationSetName")[0] configuration_set_name = self.querystring.get("ConfigurationSetName")[0] # type: ignore
self.backend.describe_configuration_set(configuration_set_name) self.backend.describe_configuration_set(configuration_set_name)
template = self.response_template(DESCRIBE_CONFIGURATION_SET) template = self.response_template(DESCRIBE_CONFIGURATION_SET)
return template.render(name=configuration_set_name) return template.render(name=configuration_set_name)
def create_configuration_set_event_destination(self): def create_configuration_set_event_destination(self) -> str:
configuration_set_name = self._get_param("ConfigurationSetName") configuration_set_name = self._get_param("ConfigurationSetName") # type: ignore
is_configuration_event_enabled = self.querystring.get( is_configuration_event_enabled = self.querystring.get(
"EventDestination.Enabled" "EventDestination.Enabled"
)[0] )[
configuration_event_name = self.querystring.get("EventDestination.Name")[0] 0
event_topic_arn = self.querystring.get( ] # type: ignore
configuration_event_name = self.querystring.get("EventDestination.Name")[0] # type: ignore
event_topic_arn = self.querystring.get( # type: ignore
"EventDestination.SNSDestination.TopicARN" "EventDestination.SNSDestination.TopicARN"
)[0] )[0]
event_matching_types = self._get_multi_param( event_matching_types = self._get_multi_param(
@ -230,7 +242,7 @@ class EmailResponse(BaseResponse):
template = self.response_template(CREATE_CONFIGURATION_SET_EVENT_DESTINATION) template = self.response_template(CREATE_CONFIGURATION_SET_EVENT_DESTINATION)
return template.render() return template.render()
def create_template(self): def create_template(self) -> str:
template_data = self._get_dict_param("Template") template_data = self._get_dict_param("Template")
template_info = {} template_info = {}
template_info["text_part"] = template_data.get("._text_part", "") template_info["text_part"] = template_data.get("._text_part", "")
@ -242,7 +254,7 @@ class EmailResponse(BaseResponse):
template = self.response_template(CREATE_TEMPLATE) template = self.response_template(CREATE_TEMPLATE)
return template.render() return template.render()
def update_template(self): def update_template(self) -> str:
template_data = self._get_dict_param("Template") template_data = self._get_dict_param("Template")
template_info = {} template_info = {}
template_info["text_part"] = template_data.get("._text_part", "") template_info["text_part"] = template_data.get("._text_part", "")
@ -254,43 +266,43 @@ class EmailResponse(BaseResponse):
template = self.response_template(UPDATE_TEMPLATE) template = self.response_template(UPDATE_TEMPLATE)
return template.render() return template.render()
def get_template(self): def get_template(self) -> str:
template_name = self._get_param("TemplateName") template_name = self._get_param("TemplateName")
template_data = self.backend.get_template(template_name) template_data = self.backend.get_template(template_name)
template = self.response_template(GET_TEMPLATE) template = self.response_template(GET_TEMPLATE)
return template.render(template_data=template_data) return template.render(template_data=template_data)
def list_templates(self): def list_templates(self) -> str:
email_templates = self.backend.list_templates() email_templates = self.backend.list_templates()
template = self.response_template(LIST_TEMPLATES) template = self.response_template(LIST_TEMPLATES)
return template.render(templates=email_templates) return template.render(templates=email_templates)
def test_render_template(self): def test_render_template(self) -> str:
render_info = self._get_dict_param("Template") render_info = self._get_dict_param("Template")
rendered_template = self.backend.render_template(render_info) rendered_template = self.backend.render_template(render_info)
template = self.response_template(RENDER_TEMPLATE) template = self.response_template(RENDER_TEMPLATE)
return template.render(template=rendered_template) return template.render(template=rendered_template)
def create_receipt_rule_set(self): def create_receipt_rule_set(self) -> str:
rule_set_name = self._get_param("RuleSetName") rule_set_name = self._get_param("RuleSetName")
self.backend.create_receipt_rule_set(rule_set_name) self.backend.create_receipt_rule_set(rule_set_name)
template = self.response_template(CREATE_RECEIPT_RULE_SET) template = self.response_template(CREATE_RECEIPT_RULE_SET)
return template.render() return template.render()
def create_receipt_rule(self): def create_receipt_rule(self) -> str:
rule_set_name = self._get_param("RuleSetName") rule_set_name = self._get_param("RuleSetName")
rule = self._get_dict_param("Rule.") rule = self._get_dict_param("Rule.")
self.backend.create_receipt_rule(rule_set_name, rule) self.backend.create_receipt_rule(rule_set_name, rule)
template = self.response_template(CREATE_RECEIPT_RULE) template = self.response_template(CREATE_RECEIPT_RULE)
return template.render() return template.render()
def describe_receipt_rule_set(self): def describe_receipt_rule_set(self) -> str:
rule_set_name = self._get_param("RuleSetName") rule_set_name = self._get_param("RuleSetName")
rule_set = self.backend.describe_receipt_rule_set(rule_set_name) rule_set = self.backend.describe_receipt_rule_set(rule_set_name)
for i, rule in enumerate(rule_set): for i, rule in enumerate(rule_set):
formatted_rule = {} formatted_rule: Dict[str, Any] = {}
for k, v in rule.items(): for k, v in rule.items():
self._parse_param(k, v, formatted_rule) self._parse_param(k, v, formatted_rule)
@ -301,13 +313,13 @@ class EmailResponse(BaseResponse):
return template.render(rule_set=rule_set, rule_set_name=rule_set_name) return template.render(rule_set=rule_set, rule_set_name=rule_set_name)
def describe_receipt_rule(self): def describe_receipt_rule(self) -> str:
rule_set_name = self._get_param("RuleSetName") rule_set_name = self._get_param("RuleSetName")
rule_name = self._get_param("RuleName") rule_name = self._get_param("RuleName")
receipt_rule = self.backend.describe_receipt_rule(rule_set_name, rule_name) receipt_rule = self.backend.describe_receipt_rule(rule_set_name, rule_name)
rule = {} rule: Dict[str, Any] = {}
for k, v in receipt_rule.items(): for k, v in receipt_rule.items():
self._parse_param(k, v, rule) self._parse_param(k, v, rule)
@ -315,7 +327,7 @@ class EmailResponse(BaseResponse):
template = self.response_template(DESCRIBE_RECEIPT_RULE) template = self.response_template(DESCRIBE_RECEIPT_RULE)
return template.render(rule=rule) return template.render(rule=rule)
def update_receipt_rule(self): def update_receipt_rule(self) -> str:
rule_set_name = self._get_param("RuleSetName") rule_set_name = self._get_param("RuleSetName")
rule = self._get_dict_param("Rule.") rule = self._get_dict_param("Rule.")
@ -324,7 +336,7 @@ class EmailResponse(BaseResponse):
template = self.response_template(UPDATE_RECEIPT_RULE) template = self.response_template(UPDATE_RECEIPT_RULE)
return template.render() return template.render()
def set_identity_mail_from_domain(self): def set_identity_mail_from_domain(self) -> str:
identity = self._get_param("Identity") identity = self._get_param("Identity")
mail_from_domain = self._get_param("MailFromDomain") mail_from_domain = self._get_param("MailFromDomain")
behavior_on_mx_failure = self._get_param("BehaviorOnMXFailure") behavior_on_mx_failure = self._get_param("BehaviorOnMXFailure")
@ -336,14 +348,16 @@ class EmailResponse(BaseResponse):
template = self.response_template(SET_IDENTITY_MAIL_FROM_DOMAIN) template = self.response_template(SET_IDENTITY_MAIL_FROM_DOMAIN)
return template.render() return template.render()
def get_identity_mail_from_domain_attributes(self): def get_identity_mail_from_domain_attributes(self) -> str:
identities = self._get_multi_param("Identities.member.") identities = self._get_multi_param("Identities.member.")
identities = self.backend.get_identity_mail_from_domain_attributes(identities) attributes_by_identity = self.backend.get_identity_mail_from_domain_attributes(
identities
)
template = self.response_template(GET_IDENTITY_MAIL_FROM_DOMAIN_ATTRIBUTES) template = self.response_template(GET_IDENTITY_MAIL_FROM_DOMAIN_ATTRIBUTES)
return template.render(identities=identities) return template.render(identities=attributes_by_identity)
def get_identity_verification_attributes(self): def get_identity_verification_attributes(self) -> str:
params = self._get_params() params = self._get_params()
identities = params.get("Identities") identities = params.get("Identities")
verification_attributes = self.backend.get_identity_verification_attributes( verification_attributes = self.backend.get_identity_verification_attributes(

View File

@ -1,8 +1,15 @@
from typing import Any, Dict, Optional
from .exceptions import MissingRenderingAttributeException from .exceptions import MissingRenderingAttributeException
from moto.utilities.tokenizer import GenericTokenizer from moto.utilities.tokenizer import GenericTokenizer
def parse_template(template, template_data, tokenizer=None, until=None): def parse_template(
template: str,
template_data: Dict[str, Any],
tokenizer: Optional[GenericTokenizer] = None,
until: Optional[str] = None,
) -> str:
tokenizer = tokenizer or GenericTokenizer(template) tokenizer = tokenizer or GenericTokenizer(template)
state = "" state = ""
parsed = "" parsed = ""
@ -27,7 +34,7 @@ def parse_template(template, template_data, tokenizer=None, until=None):
if state == "VAR": if state == "VAR":
if template_data.get(var_name) is None: if template_data.get(var_name) is None:
raise MissingRenderingAttributeException(var_name) raise MissingRenderingAttributeException(var_name)
parsed += template_data.get(var_name) parsed += template_data.get(var_name) # type: ignore
else: else:
current_position = tokenizer.token_pos current_position = tokenizer.token_pos
for item in template_data.get(var_name, []): for item in template_data.get(var_name, []):

View File

@ -1,19 +1,21 @@
import string import string
from typing import Optional
from email.utils import parseaddr from email.utils import parseaddr
from moto.moto_api._internal import mock_random as random from moto.moto_api._internal import mock_random as random
def random_hex(length): def random_hex(length: int) -> str:
return "".join(random.choice(string.ascii_lowercase) for x in range(length)) return "".join(random.choice(string.ascii_lowercase) for x in range(length))
def get_random_message_id(): def get_random_message_id() -> str:
return f"{random_hex(16)}-{random_hex(8)}-{random_hex(4)}-{random_hex(4)}-{random_hex(4)}-{random_hex(12)}-{random_hex(6)}" return f"{random_hex(16)}-{random_hex(8)}-{random_hex(4)}-{random_hex(4)}-{random_hex(4)}-{random_hex(12)}-{random_hex(6)}"
def is_valid_address(addr): def is_valid_address(addr: str) -> Optional[str]:
_, address = parseaddr(addr) _, address = parseaddr(addr)
address = address.split("@") address_parts = address.split("@")
if len(address) != 2 or not address[1]: if len(address_parts) != 2 or not address_parts[1]:
return False, "Missing domain" return "Missing domain"
return True, None return None

View File

@ -239,7 +239,7 @@ disable = W,C,R,E
enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import enable = anomalous-backslash-in-string, arguments-renamed, dangerous-default-value, deprecated-module, function-redefined, import-self, redefined-builtin, redefined-outer-name, reimported, pointless-statement, super-with-arguments, unused-argument, unused-import, unused-variable, useless-else-on-loop, wildcard-import
[mypy] [mypy]
files= moto/a*,moto/b*,moto/c*,moto/d*,moto/e*,moto/f*,moto/g*,moto/i*,moto/k*,moto/l*,moto/m*,moto/n*,moto/o*,moto/p*,moto/q*,moto/r*,moto/s3*,moto/sagemaker,moto/secretsmanager,moto/sqs,moto/ssm,moto/scheduler files= moto/a*,moto/b*,moto/c*,moto/d*,moto/e*,moto/f*,moto/g*,moto/i*,moto/k*,moto/l*,moto/m*,moto/n*,moto/o*,moto/p*,moto/q*,moto/r*,moto/s3*,moto/sagemaker,moto/secretsmanager,moto/ses,moto/sqs,moto/ssm,moto/scheduler
show_column_numbers=True show_column_numbers=True
show_error_codes = True show_error_codes = True
disable_error_code=abstract disable_error_code=abstract

View File

@ -4,14 +4,11 @@ from moto.ses.utils import is_valid_address
def test_is_valid_address(): def test_is_valid_address():
valid, msg = is_valid_address("test@example.com") msg = is_valid_address("test@example.com")
valid.should.equal(True)
msg.should.equal(None) msg.should.equal(None)
valid, msg = is_valid_address("test@") msg = is_valid_address("test@")
valid.should.equal(False)
msg.should.be.a(str) msg.should.be.a(str)
valid, msg = is_valid_address("test") msg = is_valid_address("test")
valid.should.equal(False)
msg.should.be.a(str) msg.should.be.a(str)