Repository: getmoto/moto

Issue:
SES  raise MessageRejectedError() seems to result in error message "null" on the client side (Java SES SDK v2)
I'm using the moto docker container to test an SES integration I've created.

Whenever I tried to send an email I got a 400 back with error message "null".
After poking around in it I realize I ended up in 

https://github.com/getmoto/moto/blob/2e9b903ab6a4d1e2309543519249b85fd81e319b/moto/ses/models.py#L276

```
        if not self._is_verified_address(source):
            self.rejected_messages_count += 1
            raise MessageRejectedError(f"Email address not verified {source}")
```
Adding the source address as verified solved the problem and I could send emails.

So it seems like the string feed to the constructor isn't properly mapped into the response.

To reproduce: 

```java

        Message message = Message
                .builder()
                .subject(Content.builder().charset("UTF-8").data("subject").build())
                .body(Body.builder().text(Content.builder().charset("UTF-8").data("body").build()).build())
                .build();
        
        SendEmailRequest request = SendEmailRequest
                .builder()
                .source("noreply@thedomain.com")
                .destination(Destination.builder().toAddresses(List.of("one@one.com")).build())
                .message(message)
                .build();

        SendEmailResponse sendEmailResponse = sesClient.sendEmail(request);

```
The expectation here is that i would get error message "Email address not verified noreply@thedomain.com" back.

Now, if I add 

```
sesClient.verifyEmailAddress(VerifyEmailAddressRequest.builder().emailAddress("noreply@thedomain.com").build());
```
before sending the email it succeeds. Thereof my conclusion that the error message is not properly mapped into the response.

Moto image version motoserver/moto:5.0.1
SES Java SDK version: 2.24.0 (tried with 2.23.0 too)

As a last comment: a few log statements in the ses implementation would be very useful. It's awfully silent.

---- 
EDIT: I've dug into the SDK and I can see that the error message is in the response, but not where the response parser expects it to be.

The code looks like this:

```
    Optional<XmlElement> getErrorRoot(XmlElement document) {
        return document.getOptionalElementByName("Error");
    }
```
This will return and empty optional, that is the expected element "Error" does not exist. Instead Moto places the error in `Errors[].Error.Message`    

Ref: https://github.com/aws/aws-sdk-java-v2/blob/21f9e2d1e0781dbe50249e9c0281b7315382be14/core/protocols/aws-query-protocol/src/main/java/software/amazon/awssdk/protocols/query/AwsQueryProtocolFactory.java#L116

Since `AwsQueryProtocolFactory` seems to be shared among many of the SDKs I imagine this would happen in quite some places - not just in the SES part.



Discussion:
Hi @code-and-such, thanks for raising this, and for figuring out what's wrong. Most of our internal tests use the Python SDK, which is a more lenient on the exact XML format that can be produced - that's why we will sometimes return a response in the wrong format without noticing.

Marking it as a bug.

Resolving patch:
diff --git a/moto/ses/exceptions.py b/moto/ses/exceptions.py
--- a/moto/ses/exceptions.py
+++ b/moto/ses/exceptions.py
@@ -1,100 +1,82 @@
+from typing import Any
+
 from moto.core.exceptions import RESTError
 
 
-class MessageRejectedError(RESTError):
+class SesError(RESTError):
     code = 400
 
+    def __init__(self, error_type: str, message: str, *args: Any, **kwargs: Any):
+        kwargs.setdefault("template", "wrapped_single_error")
+        super().__init__(error_type, message, *args, **kwargs)
+
+
+class MessageRejectedError(SesError):
     def __init__(self, message: str):
         super().__init__("MessageRejected", message)
 
 
-class ConfigurationSetDoesNotExist(RESTError):
-    code = 400
-
+class ConfigurationSetDoesNotExist(SesError):
     def __init__(self, message: str):
         super().__init__("ConfigurationSetDoesNotExist", message)
 
 
-class ConfigurationSetAlreadyExists(RESTError):
-    code = 400
-
+class ConfigurationSetAlreadyExists(SesError):
     def __init__(self, message: str):
         super().__init__("ConfigurationSetAlreadyExists", message)
 
 
-class EventDestinationAlreadyExists(RESTError):
-    code = 400
-
+class EventDestinationAlreadyExists(SesError):
     def __init__(self, message: str):
         super().__init__("EventDestinationAlreadyExists", message)
 
 
-class TemplateNameAlreadyExists(RESTError):
-    code = 400
-
+class TemplateNameAlreadyExists(SesError):
     def __init__(self, message: str):
         super().__init__("TemplateNameAlreadyExists", message)
 
 
-class ValidationError(RESTError):
-    code = 400
-
+class ValidationError(SesError):
     def __init__(self, message: str):
         super().__init__("ValidationError", message)
 
 
-class InvalidParameterValue(RESTError):
-    code = 400
-
+class InvalidParameterValue(SesError):
     def __init__(self, message: str):
         super().__init__("InvalidParameterValue", message)
 
 
-class InvalidRenderingParameterException(RESTError):
-    code = 400
-
+class InvalidRenderingParameterException(SesError):
     def __init__(self, message: str):
         super().__init__("InvalidRenderingParameterException", message)
 
 
-class TemplateDoesNotExist(RESTError):
-    code = 400
-
+class TemplateDoesNotExist(SesError):
     def __init__(self, message: str):
         super().__init__("TemplateDoesNotExist", message)
 
 
-class RuleSetNameAlreadyExists(RESTError):
-    code = 400
-
+class RuleSetNameAlreadyExists(SesError):
     def __init__(self, message: str):
         super().__init__("RuleSetNameAlreadyExists", message)
 
 
-class RuleAlreadyExists(RESTError):
-    code = 400
-
+class RuleAlreadyExists(SesError):
     def __init__(self, message: str):
         super().__init__("RuleAlreadyExists", message)
 
 
-class RuleSetDoesNotExist(RESTError):
-    code = 400
-
+class RuleSetDoesNotExist(SesError):
     def __init__(self, message: str):
         super().__init__("RuleSetDoesNotExist", message)
 
 
-class RuleDoesNotExist(RESTError):
-    code = 400
-
+class RuleDoesNotExist(SesError):
     def __init__(self, message: str):
         super().__init__("RuleDoesNotExist", message)
 
 
-class MissingRenderingAttributeException(RESTError):
-    code = 400
-
+class MissingRenderingAttributeException(SesError):
     def __init__(self, var: str):
         super().__init__(
             "MissingRenderingAttributeException",


Test patch:
diff --git a/other_langs/tests_java/pom.xml b/other_langs/tests_java/pom.xml
--- a/other_langs/tests_java/pom.xml
+++ b/other_langs/tests_java/pom.xml
@@ -37,9 +37,13 @@
       <scope>test</scope>
     </dependency>
     <dependency>
-    <groupId>software.amazon.awssdk</groupId>
-    <artifactId>dynamodb</artifactId>
-  </dependency>
+      <groupId>software.amazon.awssdk</groupId>
+      <artifactId>dynamodb</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>software.amazon.awssdk</groupId>
+      <artifactId>ses</artifactId>
+    </dependency>
     <dependency>
       <groupId>software.amazon.awssdk</groupId>
       <artifactId>s3</artifactId>
diff --git a/other_langs/tests_java/src/main/java/moto/tests/DependencyFactory.java b/other_langs/tests_java/src/main/java/moto/tests/DependencyFactory.java
--- a/other_langs/tests_java/src/main/java/moto/tests/DependencyFactory.java
+++ b/other_langs/tests_java/src/main/java/moto/tests/DependencyFactory.java
@@ -6,6 +6,7 @@
 import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient;
 import software.amazon.awssdk.regions.Region;
 import software.amazon.awssdk.services.s3.S3Client;
+import software.amazon.awssdk.services.ses.SesClient;
 import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
 import software.amazon.awssdk.utils.AttributeMap;
 
@@ -32,6 +33,14 @@ public static S3Client s3Client() {
                 .build();
     }
 
+    public static SesClient sesClient() {
+        return SesClient.builder()
+                .region(Region.US_EAST_1)
+                .httpClientBuilder(ApacheHttpClient.builder())
+                .endpointOverride(MOTO_URI)
+                .build();
+    }
+
     public static DynamoDbClient dynamoClient() {
         final AttributeMap attributeMap = AttributeMap.builder()
                 .put(SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true)
diff --git a/other_langs/tests_java/src/test/java/moto/tests/SESTest.java b/other_langs/tests_java/src/test/java/moto/tests/SESTest.java
new file mode 100644
--- /dev/null
+++ b/other_langs/tests_java/src/test/java/moto/tests/SESTest.java
@@ -0,0 +1,45 @@
+package moto.tests;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import software.amazon.awssdk.services.ses.SesClient;
+import software.amazon.awssdk.services.ses.model.Body;
+import software.amazon.awssdk.services.ses.model.Content;
+import software.amazon.awssdk.services.ses.model.Destination;
+import software.amazon.awssdk.services.ses.model.Message;
+import software.amazon.awssdk.services.ses.model.SendEmailRequest;
+import software.amazon.awssdk.services.ses.model.SendEmailResponse;
+import software.amazon.awssdk.services.ses.model.SesException;
+
+public class SESTest {
+
+    @Test
+    public void testUnverifiedEmail() {
+        SesClient sesClient = DependencyFactory.sesClient();
+
+        Message message = Message
+                .builder()
+                .subject(Content.builder().charset("UTF-8").data("subject").build())
+                .body(Body.builder().text(Content.builder().charset("UTF-8").data("body").build()).build())
+                .build();
+
+        SendEmailRequest request = SendEmailRequest
+                .builder()
+                .source("noreply@thedomain.com")
+                .destination(Destination.builder().toAddresses(List.of("one@one.com")).build())
+                .message(message)
+                .build();
+
+        try {
+            System.out.println("Attempting to send an email...");
+            sesClient.sendEmail(request);
+
+        } catch (SesException e) {
+            assertEquals(e.awsErrorDetails().errorMessage(), "Email address not verified noreply@thedomain.com");
+        }
+    }
+}