Repository: getmoto/moto

Issue:
Unverified SES email can produce invalid XML response from Moto
Moto Version: 2.0.7 installed into a virtualenv via pipenv

## Problem

This problem came up when I was attempting to unit test the behavior of my application when it attempts to send an email via SES from an unverified address (should produce a MessageRejected exception). However, I did not get a MessageRejected exception as expected, because it turns out Moto is producing invalid XML for the MessageRejected exception in this case. Which instead triggers a botocore.parsers.ResponseParserError exception:

```
Traceback (most recent call last):
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/client.py", line 386, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/client.py", line 692, in _make_api_call
    operation_model, request_dict, request_context)
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/client.py", line 711, in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/endpoint.py", line 102, in make_request
    return self._send_request(request_dict, operation_model)
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/endpoint.py", line 135, in _send_request
    request, operation_model, context)
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/endpoint.py", line 167, in _get_response
    request, operation_model)
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/endpoint.py", line 218, in _do_get_response
    response_dict, operation_model.output_shape)
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/parsers.py", line 243, in parse
    parsed = self._do_error_parse(response, shape)
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/parsers.py", line 505, in _do_error_parse
    root = self._parse_xml_string_to_dom(xml_contents)
  File "/root/.local/share/virtualenvs/testmoto-zOW-GSrY/lib/python3.7/site-packages/botocore/parsers.py", line 457, in _parse_xml_string_to_dom
    (e, xml_string))
botocore.parsers.ResponseParserError: Unable to parse response (not well-formed (invalid token): line 6, column 62), invalid XML received. Further retries may succeed:
b'<?xml version="1.0" encoding="UTF-8"?>\n  <ErrorResponse>\n    <Errors>\n      <Error>\n        <Code>MessageRejected</Code>\n        <Message>Email address not verified John Smith <foobar@example.com></Message>\n        \n      </Error>\n    </Errors>\n  <RequestID>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestID>\n</ErrorResponse>'
```

Looking at the moto code, it appears to me the problem is that the email given (specifically, the brackets in the address)  `John Smith <foobar@example.com>` is not properly escaped for XML. For example, one possible fix might be to XML escape here, instead of just putting it blindly into the message:

https://github.com/spulec/moto/blob/master/moto/ses/models.py#L161
```
class SESBackend(BaseBackend):
    ...
    def send_email(self, source, subject, body, destinations, region):
        ...
        if not self._is_verified_address(source):
            self.rejected_messages_count += 1
            raise MessageRejectedError("Email address not verified %s" % source) # XML-escape source here
```

The implication of this is that I am unable to properly unit test the path of my code that handles a MessageRejected error for unverified source address.

# Recreating

Here's a sample of calling boto3.client("ses").send_email which would trigger the error in Moto if "example.com" is not a verified address (eg. if I have not called something like `boto3.client("ses").verify_domain_identity("example.com")`):
```python3
boto3.client("ses").send_email(
    Source=f"John Smith <foobar@example.com>",   # << Unverified source address
    Destination={
        'ToAddresses': ["blah@example.com"],
        'CcAddresses': [],
        'BccAddresses': []
    }, Message={
        'Subject': {'Data': "Hello!"},
        'Body': {'Html': {'Data': "<html>Hi</html>"}}
    }, SourceArn=source_arn,
    ReplyToAddresses=[]
)
```


Discussion:
Thanks for raising this, and for providing a reproducible scenario @amacf! Marking it as a bug.

Resolving patch:
diff --git a/moto/core/exceptions.py b/moto/core/exceptions.py
--- a/moto/core/exceptions.py
+++ b/moto/core/exceptions.py
@@ -31,7 +31,7 @@
     <Errors>
       <Error>
         <Code>{{error_type}}</Code>
-        <Message>{{message}}</Message>
+        <Message><![CDATA[{{message}}]]></Message>
         {% block extra %}{% endblock %}
       </Error>
     </Errors>


Test patch:
diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py
--- a/tests/test_ses/test_ses_boto3.py
+++ b/tests/test_ses/test_ses_boto3.py
@@ -114,6 +114,32 @@ def test_send_email_when_verify_source():
     sent_count.should.equal(2)
 
 
+@mock_ses
+def test_send_unverified_email_with_chevrons():
+    conn = boto3.client("ses", region_name="us-east-1")
+
+    # Sending an email to an unverified source should fail
+    with pytest.raises(ClientError) as ex:
+        conn.send_email(
+            Source=f"John Smith <foobar@example.com>",  # << Unverified source address
+            Destination={
+                "ToAddresses": ["blah@example.com"],
+                "CcAddresses": [],
+                "BccAddresses": [],
+            },
+            Message={
+                "Subject": {"Data": "Hello!"},
+                "Body": {"Html": {"Data": "<html>Hi</html>"}},
+            },
+        )
+    err = ex.value.response["Error"]
+    err["Code"].should.equal("MessageRejected")
+    # The source should be returned exactly as provided - without XML encoding issues
+    err["Message"].should.equal(
+        "Email address not verified John Smith <foobar@example.com>"
+    )
+
+
 @mock_ses
 def test_send_templated_email():
     conn = boto3.client("ses", region_name="us-east-1")