Repository: cisagov/manage.get.gov

Issue:
help@get.gov is receiving lots of bouncebacks for failed test emails
Our new program inbox will be help@get.gov, hosted in M365. We are also sending automated registrar emails from this address via SES. SES is doing what it does and actually attempting to deliver test emails we're sending to fake addresses, which bounces-back into our real inbox.

Though we can filter out bounced emails from amazonses.com, let's dig into this and see if we can limit the bouncebacks or try another approach.

- [ ] Find the cause of the emails (dig into sending logic, SES logs)
- [ ] Decide on a course of action (create actual test inboxes?)
- [ ] Make a change and confirm impact
- [ ] fixtures and tests should not result in an email being sent

Check out our [Slack thread](https://cisa-corp.slack.com/archives/C05BJTNCY12/p1695764077201119) about it.


Discussion:
These test cases don't appear to be using boto3 mocking:

**test_admin.py:** `test_save_model_sets_restricted_status_on_user` and `test_save_model_sets_approved_domain`

**test_views.py:** `test_domain_user_add`, `test_domain_invitation_cancel`, `test_domain_invitation_cancel_no_permissions`, and `test_domain_security_email_form`
The volume got so high this past weekend that M365 rate-limited the inbox. (Still a later.)
For dev context: there everytime fixtures is run we are sending emails. In addition to the test cases called out, we need to make sure that fixtures doesn't result in sending emails, which will most likely need to be a setting that gets passed on those relevent functions such as .approve(send_email=False)

Resolving patch:
diff --git a/src/registrar/fixtures_applications.py b/src/registrar/fixtures_applications.py
--- a/src/registrar/fixtures_applications.py
+++ b/src/registrar/fixtures_applications.py
@@ -218,5 +218,8 @@ def load(cls):
                 creator=user, status=DomainApplication.ApplicationStatus.IN_REVIEW
             ).last()
             logger.debug(f"Approving {application} for {user}")
-            application.approve()
+
+            # We don't want fixtures sending out real emails to
+            # fake email addresses, so we just skip that and log it instead
+            application.approve(send_email=False)
             application.save()
diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py
--- a/src/registrar/models/domain_application.py
+++ b/src/registrar/models/domain_application.py
@@ -570,17 +570,25 @@ def domain_is_not_active(self):
             return not self.approved_domain.is_active()
         return True
 
-    def _send_status_update_email(self, new_status, email_template, email_template_subject):
-        """Send a atatus update email to the submitter.
+    def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True):
+        """Send a status update email to the submitter.
 
         The email goes to the email address that the submitter gave as their
         contact information. If there is not submitter information, then do
         nothing.
+
+        send_email: bool -> Used to bypass the send_templated_email function, in the event
+        we just want to log that an email would have been sent, rather than actually sending one.
         """
 
         if self.submitter is None or self.submitter.email is None:
             logger.warning(f"Cannot send {new_status} email, no submitter email address.")
-            return
+            return None
+
+        if not send_email:
+            logger.info(f"Email was not sent. Would send {new_status} email: {self.submitter.email}")
+            return None
+
         try:
             send_templated_email(
                 email_template,
@@ -684,7 +692,7 @@ def action_needed(self):
         ],
         target=ApplicationStatus.APPROVED,
     )
-    def approve(self):
+    def approve(self, send_email=True):
         """Approve an application that has been submitted.
 
         This has substantial side-effects because it creates another database
@@ -713,6 +721,7 @@ def approve(self):
             "application approved",
             "emails/status_change_approved.txt",
             "emails/status_change_approved_subject.txt",
+            send_email,
         )
 
     @transition(
diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py
--- a/src/registrar/utility/email.py
+++ b/src/registrar/utility/email.py
@@ -1,11 +1,14 @@
 """Utilities for sending emails."""
 
 import boto3
-
+import logging
 from django.conf import settings
 from django.template.loader import get_template
 
 
+logger = logging.getLogger(__name__)
+
+
 class EmailSendingError(RuntimeError):
 
     """Local error for handling all failures when sending email."""
@@ -20,7 +23,7 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr
     context as Django's HTML templates. context gives additional information
     that the template may use.
     """
-
+    logger.info(f"An email was sent! Template name: {template_name} to {to_address}")
     template = get_template(template_name)
     email_body = template.render(context=context)
 


Test patch:
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -18,9 +18,11 @@
 from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
 from registrar.models.user_domain_role import UserDomainRole
 from .common import (
+    MockSESClient,
     AuditedAdminMockData,
     completed_application,
     generic_domain_object,
+    less_console_noise,
     mock_user,
     create_superuser,
     create_user,
@@ -35,7 +37,6 @@
 from unittest import skip
 
 from django.conf import settings
-from unittest.mock import MagicMock
 import boto3_mocking  # type: ignore
 import logging
 
@@ -58,7 +59,10 @@ def test_short_org_name_in_domains_list(self):
         """
         self.client.force_login(self.superuser)
         application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
-        application.approve()
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():
+                application.approve()
 
         response = self.client.get("/admin/registrar/domain/")
 
@@ -326,6 +330,7 @@ def setUp(self):
             url="/admin/registrar/DomainApplication/",
             model=DomainApplication,
         )
+        self.mock_client = MockSESClient()
 
     def test_domain_sortable(self):
         """Tests if the DomainApplication sorts by domain correctly"""
@@ -420,25 +425,23 @@ def test_save_model_sends_submitted_email(self):
         EMAIL = "mayor@igorville.gov"
         User.objects.filter(email=EMAIL).delete()
 
-        mock_client = MagicMock()
-        mock_client_instance = mock_client.return_value
-
-        with boto3_mocking.clients.handler_for("sesv2", mock_client):
-            # Create a sample application
-            application = completed_application()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # Create a sample application
+                application = completed_application()
 
-            # Create a mock request
-            request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
+                # Create a mock request
+                request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
 
-            # Modify the application's property
-            application.status = DomainApplication.ApplicationStatus.SUBMITTED
+                # Modify the application's property
+                application.status = DomainApplication.ApplicationStatus.SUBMITTED
 
-            # Use the model admin's save_model method
-            self.admin.save_model(request, application, form=None, change=True)
+                # Use the model admin's save_model method
+                self.admin.save_model(request, application, form=None, change=True)
 
         # Access the arguments passed to send_email
-        call_args = mock_client_instance.send_email.call_args
-        args, kwargs = call_args
+        call_args = self.mock_client.EMAILS_SENT
+        kwargs = call_args[0]["kwargs"]
 
         # Retrieve the email details from the arguments
         from_email = kwargs.get("FromEmailAddress")
@@ -452,8 +455,7 @@ def test_save_model_sends_submitted_email(self):
         self.assertEqual(to_email, EMAIL)
         self.assertIn(expected_string, email_body)
 
-        # Perform assertions on the mock call itself
-        mock_client_instance.send_email.assert_called_once()
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
 
     @boto3_mocking.patching
     def test_save_model_sends_in_review_email(self):
@@ -461,25 +463,23 @@ def test_save_model_sends_in_review_email(self):
         EMAIL = "mayor@igorville.gov"
         User.objects.filter(email=EMAIL).delete()
 
-        mock_client = MagicMock()
-        mock_client_instance = mock_client.return_value
-
-        with boto3_mocking.clients.handler_for("sesv2", mock_client):
-            # Create a sample application
-            application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # Create a sample application
+                application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED)
 
-            # Create a mock request
-            request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
+                # Create a mock request
+                request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
 
-            # Modify the application's property
-            application.status = DomainApplication.ApplicationStatus.IN_REVIEW
+                # Modify the application's property
+                application.status = DomainApplication.ApplicationStatus.IN_REVIEW
 
-            # Use the model admin's save_model method
-            self.admin.save_model(request, application, form=None, change=True)
+                # Use the model admin's save_model method
+                self.admin.save_model(request, application, form=None, change=True)
 
         # Access the arguments passed to send_email
-        call_args = mock_client_instance.send_email.call_args
-        args, kwargs = call_args
+        call_args = self.mock_client.EMAILS_SENT
+        kwargs = call_args[0]["kwargs"]
 
         # Retrieve the email details from the arguments
         from_email = kwargs.get("FromEmailAddress")
@@ -493,8 +493,7 @@ def test_save_model_sends_in_review_email(self):
         self.assertEqual(to_email, EMAIL)
         self.assertIn(expected_string, email_body)
 
-        # Perform assertions on the mock call itself
-        mock_client_instance.send_email.assert_called_once()
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
 
     @boto3_mocking.patching
     def test_save_model_sends_approved_email(self):
@@ -502,25 +501,23 @@ def test_save_model_sends_approved_email(self):
         EMAIL = "mayor@igorville.gov"
         User.objects.filter(email=EMAIL).delete()
 
-        mock_client = MagicMock()
-        mock_client_instance = mock_client.return_value
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # Create a sample application
+                application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
 
-        with boto3_mocking.clients.handler_for("sesv2", mock_client):
-            # Create a sample application
-            application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
-
-            # Create a mock request
-            request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
+                # Create a mock request
+                request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
 
-            # Modify the application's property
-            application.status = DomainApplication.ApplicationStatus.APPROVED
+                # Modify the application's property
+                application.status = DomainApplication.ApplicationStatus.APPROVED
 
-            # Use the model admin's save_model method
-            self.admin.save_model(request, application, form=None, change=True)
+                # Use the model admin's save_model method
+                self.admin.save_model(request, application, form=None, change=True)
 
         # Access the arguments passed to send_email
-        call_args = mock_client_instance.send_email.call_args
-        args, kwargs = call_args
+        call_args = self.mock_client.EMAILS_SENT
+        kwargs = call_args[0]["kwargs"]
 
         # Retrieve the email details from the arguments
         from_email = kwargs.get("FromEmailAddress")
@@ -534,9 +531,9 @@ def test_save_model_sends_approved_email(self):
         self.assertEqual(to_email, EMAIL)
         self.assertIn(expected_string, email_body)
 
-        # Perform assertions on the mock call itself
-        mock_client_instance.send_email.assert_called_once()
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
 
+    @boto3_mocking.patching
     def test_save_model_sets_approved_domain(self):
         # make sure there is no user with this email
         EMAIL = "mayor@igorville.gov"
@@ -548,11 +545,13 @@ def test_save_model_sets_approved_domain(self):
         # Create a mock request
         request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
 
-        # Modify the application's property
-        application.status = DomainApplication.ApplicationStatus.APPROVED
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # Modify the application's property
+                application.status = DomainApplication.ApplicationStatus.APPROVED
 
-        # Use the model admin's save_model method
-        self.admin.save_model(request, application, form=None, change=True)
+                # Use the model admin's save_model method
+                self.admin.save_model(request, application, form=None, change=True)
 
         # Test that approved domain exists and equals requested domain
         self.assertEqual(application.requested_domain.name, application.approved_domain.name)
@@ -563,25 +562,23 @@ def test_save_model_sends_action_needed_email(self):
         EMAIL = "mayor@igorville.gov"
         User.objects.filter(email=EMAIL).delete()
 
-        mock_client = MagicMock()
-        mock_client_instance = mock_client.return_value
-
-        with boto3_mocking.clients.handler_for("sesv2", mock_client):
-            # Create a sample application
-            application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # Create a sample application
+                application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
 
-            # Create a mock request
-            request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
+                # Create a mock request
+                request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
 
-            # Modify the application's property
-            application.status = DomainApplication.ApplicationStatus.ACTION_NEEDED
+                # Modify the application's property
+                application.status = DomainApplication.ApplicationStatus.ACTION_NEEDED
 
-            # Use the model admin's save_model method
-            self.admin.save_model(request, application, form=None, change=True)
+                # Use the model admin's save_model method
+                self.admin.save_model(request, application, form=None, change=True)
 
         # Access the arguments passed to send_email
-        call_args = mock_client_instance.send_email.call_args
-        args, kwargs = call_args
+        call_args = self.mock_client.EMAILS_SENT
+        kwargs = call_args[0]["kwargs"]
 
         # Retrieve the email details from the arguments
         from_email = kwargs.get("FromEmailAddress")
@@ -595,8 +592,7 @@ def test_save_model_sends_action_needed_email(self):
         self.assertEqual(to_email, EMAIL)
         self.assertIn(expected_string, email_body)
 
-        # Perform assertions on the mock call itself
-        mock_client_instance.send_email.assert_called_once()
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
 
     @boto3_mocking.patching
     def test_save_model_sends_rejected_email(self):
@@ -604,25 +600,23 @@ def test_save_model_sends_rejected_email(self):
         EMAIL = "mayor@igorville.gov"
         User.objects.filter(email=EMAIL).delete()
 
-        mock_client = MagicMock()
-        mock_client_instance = mock_client.return_value
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # Create a sample application
+                application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
 
-        with boto3_mocking.clients.handler_for("sesv2", mock_client):
-            # Create a sample application
-            application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
-
-            # Create a mock request
-            request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
+                # Create a mock request
+                request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
 
-            # Modify the application's property
-            application.status = DomainApplication.ApplicationStatus.REJECTED
+                # Modify the application's property
+                application.status = DomainApplication.ApplicationStatus.REJECTED
 
-            # Use the model admin's save_model method
-            self.admin.save_model(request, application, form=None, change=True)
+                # Use the model admin's save_model method
+                self.admin.save_model(request, application, form=None, change=True)
 
         # Access the arguments passed to send_email
-        call_args = mock_client_instance.send_email.call_args
-        args, kwargs = call_args
+        call_args = self.mock_client.EMAILS_SENT
+        kwargs = call_args[0]["kwargs"]
 
         # Retrieve the email details from the arguments
         from_email = kwargs.get("FromEmailAddress")
@@ -636,9 +630,9 @@ def test_save_model_sends_rejected_email(self):
         self.assertEqual(to_email, EMAIL)
         self.assertIn(expected_string, email_body)
 
-        # Perform assertions on the mock call itself
-        mock_client_instance.send_email.assert_called_once()
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
 
+    @boto3_mocking.patching
     def test_save_model_sets_restricted_status_on_user(self):
         # make sure there is no user with this email
         EMAIL = "mayor@igorville.gov"
@@ -650,19 +644,23 @@ def test_save_model_sets_restricted_status_on_user(self):
         # Create a mock request
         request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
 
-        # Modify the application's property
-        application.status = DomainApplication.ApplicationStatus.INELIGIBLE
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # Modify the application's property
+                application.status = DomainApplication.ApplicationStatus.INELIGIBLE
 
-        # Use the model admin's save_model method
-        self.admin.save_model(request, application, form=None, change=True)
+                # Use the model admin's save_model method
+                self.admin.save_model(request, application, form=None, change=True)
 
         # Test that approved domain exists and equals requested domain
         self.assertEqual(application.creator.status, "restricted")
 
     def test_readonly_when_restricted_creator(self):
         application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
-        application.creator.status = User.RESTRICTED
-        application.creator.save()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                application.creator.status = User.RESTRICTED
+                application.creator.save()
 
         request = self.factory.get("/")
         request.user = self.superuser
@@ -740,8 +738,10 @@ def test_readonly_fields_for_superuser(self):
     def test_saving_when_restricted_creator(self):
         # Create an instance of the model
         application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
-        application.creator.status = User.RESTRICTED
-        application.creator.save()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                application.creator.status = User.RESTRICTED
+                application.creator.save()
 
         # Create a request object with a superuser
         request = self.factory.get("/")
@@ -763,8 +763,10 @@ def test_saving_when_restricted_creator(self):
     def test_change_view_with_restricted_creator(self):
         # Create an instance of the model
         application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
-        application.creator.status = User.RESTRICTED
-        application.creator.save()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                application.creator.status = User.RESTRICTED
+                application.creator.save()
 
         with patch("django.contrib.messages.warning") as mock_warning:
             # Create a request object with a superuser
@@ -779,6 +781,7 @@ def test_change_view_with_restricted_creator(self):
                 "Cannot edit an application with a restricted creator.",
             )
 
+    @boto3_mocking.patching
     def test_error_when_saving_approved_to_rejected_and_domain_is_active(self):
         # Create an instance of the model
         application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
@@ -800,9 +803,11 @@ def custom_is_active(self):
             stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
             stack.enter_context(patch.object(messages, "error"))
 
-            # Simulate saving the model
-            application.status = DomainApplication.ApplicationStatus.REJECTED
-            self.admin.save_model(request, application, None, True)
+            with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+                with less_console_noise():
+                    # Simulate saving the model
+                    application.status = DomainApplication.ApplicationStatus.REJECTED
+                    self.admin.save_model(request, application, None, True)
 
             # Assert that the error message was called with the correct argument
             messages.error.assert_called_once_with(
@@ -831,10 +836,11 @@ def custom_is_active(self):
             # Patch Domain.is_active and django.contrib.messages.error simultaneously
             stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
             stack.enter_context(patch.object(messages, "error"))
-
-            # Simulate saving the model
-            application.status = DomainApplication.ApplicationStatus.REJECTED
-            self.admin.save_model(request, application, None, True)
+            with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+                with less_console_noise():
+                    # Simulate saving the model
+                    application.status = DomainApplication.ApplicationStatus.REJECTED
+                    self.admin.save_model(request, application, None, True)
 
             # Assert that the error message was never called
             messages.error.assert_not_called()
@@ -1091,6 +1097,7 @@ def tearDown(self):
         User.objects.all().delete()
         Contact.objects.all().delete()
         Website.objects.all().delete()
+        self.mock_client.EMAILS_SENT.clear()
 
 
 class DomainInvitationAdminTest(TestCase):
diff --git a/src/registrar/tests/test_emails.py b/src/registrar/tests/test_emails.py
--- a/src/registrar/tests/test_emails.py
+++ b/src/registrar/tests/test_emails.py
@@ -3,7 +3,7 @@
 from unittest.mock import MagicMock
 
 from django.test import TestCase
-from .common import completed_application
+from .common import completed_application, less_console_noise
 
 
 import boto3_mocking  # type: ignore
@@ -20,7 +20,8 @@ def test_submission_confirmation(self):
         application = completed_application()
 
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
 
         # check that an email was sent
         self.assertTrue(self.mock_client.send_email.called)
@@ -56,7 +57,8 @@ def test_submission_confirmation_no_current_website_spacing(self):
         """Test line spacing without current_website."""
         application = completed_application(has_current_website=False)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         self.assertNotIn("Current website for your organization:", body)
@@ -68,7 +70,8 @@ def test_submission_confirmation_current_website_spacing(self):
         """Test line spacing with current_website."""
         application = completed_application(has_current_website=True)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         self.assertIn("Current website for your organization:", body)
@@ -81,7 +84,8 @@ def test_submission_confirmation_other_contacts_spacing(self):
         """Test line spacing with other contacts."""
         application = completed_application(has_other_contacts=True)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         self.assertIn("Other employees from your organization:", body)
@@ -94,7 +98,8 @@ def test_submission_confirmation_no_other_contacts_spacing(self):
         """Test line spacing without other contacts."""
         application = completed_application(has_other_contacts=False)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         self.assertNotIn("Other employees from your organization:", body)
@@ -106,7 +111,8 @@ def test_submission_confirmation_alternative_govdomain_spacing(self):
         """Test line spacing with alternative .gov domain."""
         application = completed_application(has_alternative_gov_domain=True)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         self.assertIn("city1.gov", body)
@@ -118,7 +124,8 @@ def test_submission_confirmation_no_alternative_govdomain_spacing(self):
         """Test line spacing without alternative .gov domain."""
         application = completed_application(has_alternative_gov_domain=False)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         self.assertNotIn("city1.gov", body)
@@ -130,7 +137,8 @@ def test_submission_confirmation_about_your_organization_spacing(self):
         """Test line spacing with about your organization."""
         application = completed_application(has_about_your_organization=True)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         self.assertIn("About your organization:", body)
@@ -142,7 +150,8 @@ def test_submission_confirmation_no_about_your_organization_spacing(self):
         """Test line spacing without about your organization."""
         application = completed_application(has_about_your_organization=False)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         self.assertNotIn("About your organization:", body)
@@ -154,7 +163,8 @@ def test_submission_confirmation_anything_else_spacing(self):
         """Test line spacing with anything else."""
         application = completed_application(has_anything_else=True)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         # spacing should be right between adjacent elements
@@ -165,7 +175,8 @@ def test_submission_confirmation_no_anything_else_spacing(self):
         """Test line spacing without anything else."""
         application = completed_application(has_anything_else=False)
         with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
-            application.submit()
+            with less_console_noise():
+                application.submit()
         _, kwargs = self.mock_client.send_email.call_args
         body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
         self.assertNotIn("Anything else", body)
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -19,8 +19,6 @@
 from .common import MockSESClient, less_console_noise, completed_application
 from django_fsm import TransitionNotAllowed
 
-boto3_mocking.clients.register_handler("sesv2", MockSESClient)
-
 
 # Test comment for push -- will remove
 # The DomainApplication submit method has a side effect of sending an email
@@ -53,6 +51,12 @@ def setUp(self):
             status=DomainApplication.ApplicationStatus.INELIGIBLE, name="ineligible.gov"
         )
 
+        self.mock_client = MockSESClient()
+
+    def tearDown(self):
+        super().tearDown()
+        self.mock_client.EMAILS_SENT.clear()
+
     def assertNotRaises(self, exception_type):
         """Helper method for testing allowed transitions."""
         return self.assertRaises(Exception, None, exception_type)
@@ -130,17 +134,23 @@ def test_domain_info(self):
     def test_status_fsm_submit_fail(self):
         user, _ = User.objects.get_or_create(username="testy")
         application = DomainApplication.objects.create(creator=user)
-        with self.assertRaises(ValueError):
-            # can't submit an application with a null domain name
-            application.submit()
+
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                with self.assertRaises(ValueError):
+                    # can't submit an application with a null domain name
+                    application.submit()
 
     def test_status_fsm_submit_succeed(self):
         user, _ = User.objects.get_or_create(username="testy")
         site = DraftDomain.objects.create(name="igorville.gov")
         application = DomainApplication.objects.create(creator=user, requested_domain=site)
+
         # no submitter email so this emits a log warning
-        with less_console_noise():
-            application.submit()
+
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                application.submit()
         self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED)
 
     def test_submit_sends_email(self):
@@ -154,7 +164,10 @@ def test_submit_sends_email(self):
             submitter=contact,
         )
         application.save()
-        application.submit()
+
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                application.submit()
 
         # check to see if an email was sent
         self.assertGreater(
@@ -179,12 +192,14 @@ def test_submit_transition_allowed(self):
             (self.withdrawn_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                try:
-                    application.submit()
-                except TransitionNotAllowed:
-                    self.fail("TransitionNotAllowed was raised, but it was not expected.")
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        try:
+                            application.submit()
+                        except TransitionNotAllowed:
+                            self.fail("TransitionNotAllowed was raised, but it was not expected.")
 
     def test_submit_transition_not_allowed(self):
         """
@@ -197,10 +212,12 @@ def test_submit_transition_not_allowed(self):
             (self.ineligible_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                with self.assertRaises(exception_type):
-                    application.submit()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        with self.assertRaises(exception_type):
+                            application.submit()
 
     def test_in_review_transition_allowed(self):
         """
@@ -214,12 +231,14 @@ def test_in_review_transition_allowed(self):
             (self.ineligible_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                try:
-                    application.in_review()
-                except TransitionNotAllowed:
-                    self.fail("TransitionNotAllowed was raised, but it was not expected.")
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        try:
+                            application.in_review()
+                        except TransitionNotAllowed:
+                            self.fail("TransitionNotAllowed was raised, but it was not expected.")
 
     def test_in_review_transition_not_allowed(self):
         """
@@ -231,10 +250,12 @@ def test_in_review_transition_not_allowed(self):
             (self.withdrawn_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                with self.assertRaises(exception_type):
-                    application.in_review()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        with self.assertRaises(exception_type):
+                            application.in_review()
 
     def test_action_needed_transition_allowed(self):
         """
@@ -247,12 +268,14 @@ def test_action_needed_transition_allowed(self):
             (self.ineligible_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                try:
-                    application.action_needed()
-                except TransitionNotAllowed:
-                    self.fail("TransitionNotAllowed was raised, but it was not expected.")
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        try:
+                            application.action_needed()
+                        except TransitionNotAllowed:
+                            self.fail("TransitionNotAllowed was raised, but it was not expected.")
 
     def test_action_needed_transition_not_allowed(self):
         """
@@ -265,10 +288,12 @@ def test_action_needed_transition_not_allowed(self):
             (self.withdrawn_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                with self.assertRaises(exception_type):
-                    application.action_needed()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        with self.assertRaises(exception_type):
+                            application.action_needed()
 
     def test_approved_transition_allowed(self):
         """
@@ -281,12 +306,27 @@ def test_approved_transition_allowed(self):
             (self.rejected_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                try:
-                    application.approve()
-                except TransitionNotAllowed:
-                    self.fail("TransitionNotAllowed was raised, but it was not expected.")
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        try:
+                            application.approve()
+                        except TransitionNotAllowed:
+                            self.fail("TransitionNotAllowed was raised, but it was not expected.")
+
+    def test_approved_skips_sending_email(self):
+        """
+        Test that calling .approve with send_email=False doesn't actually send
+        an email
+        """
+
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                self.submitted_application.approve(send_email=False)
+
+        # Assert that no emails were sent
+        self.assertEqual(len(self.mock_client.EMAILS_SENT), 0)
 
     def test_approved_transition_not_allowed(self):
         """
@@ -299,10 +339,12 @@ def test_approved_transition_not_allowed(self):
             (self.ineligible_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                with self.assertRaises(exception_type):
-                    application.approve()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        with self.assertRaises(exception_type):
+                            application.approve()
 
     def test_withdraw_transition_allowed(self):
         """
@@ -314,12 +356,14 @@ def test_withdraw_transition_allowed(self):
             (self.action_needed_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                try:
-                    application.withdraw()
-                except TransitionNotAllowed:
-                    self.fail("TransitionNotAllowed was raised, but it was not expected.")
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        try:
+                            application.withdraw()
+                        except TransitionNotAllowed:
+                            self.fail("TransitionNotAllowed was raised, but it was not expected.")
 
     def test_withdraw_transition_not_allowed(self):
         """
@@ -333,10 +377,12 @@ def test_withdraw_transition_not_allowed(self):
             (self.ineligible_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                with self.assertRaises(exception_type):
-                    application.withdraw()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        with self.assertRaises(exception_type):
+                            application.withdraw()
 
     def test_reject_transition_allowed(self):
         """
@@ -348,12 +394,14 @@ def test_reject_transition_allowed(self):
             (self.approved_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                try:
-                    application.reject()
-                except TransitionNotAllowed:
-                    self.fail("TransitionNotAllowed was raised, but it was not expected.")
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        try:
+                            application.reject()
+                        except TransitionNotAllowed:
+                            self.fail("TransitionNotAllowed was raised, but it was not expected.")
 
     def test_reject_transition_not_allowed(self):
         """
@@ -367,10 +415,12 @@ def test_reject_transition_not_allowed(self):
             (self.ineligible_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                with self.assertRaises(exception_type):
-                    application.reject()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        with self.assertRaises(exception_type):
+                            application.reject()
 
     def test_reject_with_prejudice_transition_allowed(self):
         """
@@ -383,12 +433,14 @@ def test_reject_with_prejudice_transition_allowed(self):
             (self.rejected_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                try:
-                    application.reject_with_prejudice()
-                except TransitionNotAllowed:
-                    self.fail("TransitionNotAllowed was raised, but it was not expected.")
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        try:
+                            application.reject_with_prejudice()
+                        except TransitionNotAllowed:
+                            self.fail("TransitionNotAllowed was raised, but it was not expected.")
 
     def test_reject_with_prejudice_transition_not_allowed(self):
         """
@@ -401,10 +453,12 @@ def test_reject_with_prejudice_transition_not_allowed(self):
             (self.ineligible_application, TransitionNotAllowed),
         ]
 
-        for application, exception_type in test_cases:
-            with self.subTest(application=application, exception_type=exception_type):
-                with self.assertRaises(exception_type):
-                    application.reject_with_prejudice()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                for application, exception_type in test_cases:
+                    with self.subTest(application=application, exception_type=exception_type):
+                        with self.assertRaises(exception_type):
+                            application.reject_with_prejudice()
 
     def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
         """Create an application with status approved, create a matching domain that
@@ -418,11 +472,13 @@ def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
         def custom_is_active(self):
             return True  # Override to return True
 
-        # Use patch to temporarily replace is_active with the custom implementation
-        with patch.object(Domain, "is_active", custom_is_active):
-            # Now, when you call is_active on Domain, it will return True
-            with self.assertRaises(TransitionNotAllowed):
-                self.approved_application.reject()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # Use patch to temporarily replace is_active with the custom implementation
+                with patch.object(Domain, "is_active", custom_is_active):
+                    # Now, when you call is_active on Domain, it will return True
+                    with self.assertRaises(TransitionNotAllowed):
+                        self.approved_application.reject()
 
     def test_transition_not_allowed_approved_ineligible_when_domain_is_active(self):
         """Create an application with status approved, create a matching domain that
@@ -436,24 +492,37 @@ def test_transition_not_allowed_approved_ineligible_when_domain_is_active(self):
         def custom_is_active(self):
             return True  # Override to return True
 
-        # Use patch to temporarily replace is_active with the custom implementation
-        with patch.object(Domain, "is_active", custom_is_active):
-            # Now, when you call is_active on Domain, it will return True
-            with self.assertRaises(TransitionNotAllowed):
-                self.approved_application.reject_with_prejudice()
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # Use patch to temporarily replace is_active with the custom implementation
+                with patch.object(Domain, "is_active", custom_is_active):
+                    # Now, when you call is_active on Domain, it will return True
+                    with self.assertRaises(TransitionNotAllowed):
+                        self.approved_application.reject_with_prejudice()
 
 
 class TestPermissions(TestCase):
-
     """Test the User-Domain-Role connection."""
 
+    def setUp(self):
+        super().setUp()
+        self.mock_client = MockSESClient()
+
+    def tearDown(self):
+        super().tearDown()
+        self.mock_client.EMAILS_SENT.clear()
+
+    @boto3_mocking.patching
     def test_approval_creates_role(self):
         draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
         user, _ = User.objects.get_or_create()
         application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
-        # skip using the submit method
-        application.status = DomainApplication.ApplicationStatus.SUBMITTED
-        application.approve()
+
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # skip using the submit method
+                application.status = DomainApplication.ApplicationStatus.SUBMITTED
+                application.approve()
 
         # should be a role for this user
         domain = Domain.objects.get(name="igorville.gov")
@@ -464,13 +533,25 @@ class TestDomainInfo(TestCase):
 
     """Test creation of Domain Information when approved."""
 
+    def setUp(self):
+        super().setUp()
+        self.mock_client = MockSESClient()
+
+    def tearDown(self):
+        super().tearDown()
+        self.mock_client.EMAILS_SENT.clear()
+
+    @boto3_mocking.patching
     def test_approval_creates_info(self):
         draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
         user, _ = User.objects.get_or_create()
         application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
-        # skip using the submit method
-        application.status = DomainApplication.ApplicationStatus.SUBMITTED
-        application.approve()
+
+        with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
+            with less_console_noise():
+                # skip using the submit method
+                application.status = DomainApplication.ApplicationStatus.SUBMITTED
+                application.approve()
 
         # should be an information present for this domain
         domain = Domain.objects.get(name="igorville.gov")
@@ -572,11 +653,12 @@ def test_check_domain_invitations_on_login_caps_email(self):
         caps_email = "MAYOR@igorville.gov"
         # mock the domain invitation save routine
         with patch("registrar.models.DomainInvitation.save") as save_mock:
-            DomainInvitation.objects.get_or_create(email=caps_email, domain=self.domain)
-            self.user.check_domain_invitations_on_login()
-            # if check_domain_invitations_on_login properly matches exactly one
-            # Domain Invitation, then save routine should be called exactly once
-            save_mock.assert_called_once()
+            with less_console_noise():
+                DomainInvitation.objects.get_or_create(email=caps_email, domain=self.domain)
+                self.user.check_domain_invitations_on_login()
+                # if check_domain_invitations_on_login properly matches exactly one
+                # Domain Invitation, then save routine should be called exactly once
+                save_mock.assert_called_once()
 
 
 class TestContact(TestCase):
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -29,8 +29,9 @@
     RegistryError,
     ErrorCode,
 )
-from .common import MockEppLib
+from .common import MockEppLib, MockSESClient, less_console_noise
 import logging
+import boto3_mocking  # type: ignore
 
 logger = logging.getLogger(__name__)
 
@@ -252,6 +253,7 @@ def test_errors_map_epp_contact_to_public_contact(self):
 class TestDomainCreation(MockEppLib):
     """Rule: An approved domain application must result in a domain"""
 
+    @boto3_mocking.patching
     def test_approved_application_creates_domain_locally(self):
         """
         Scenario: Analyst approves a domain application
@@ -262,10 +264,14 @@ def test_approved_application_creates_domain_locally(self):
         draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
         user, _ = User.objects.get_or_create()
         application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
-        # skip using the submit method
-        application.status = DomainApplication.ApplicationStatus.SUBMITTED
-        # transition to approve state
-        application.approve()
+
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():
+                # skip using the submit method
+                application.status = DomainApplication.ApplicationStatus.SUBMITTED
+                # transition to approve state
+                application.approve()
         # should have information present for this domain
         domain = Domain.objects.get(name="igorville.gov")
         self.assertTrue(domain)
diff --git a/src/registrar/tests/test_transition_domain_migrations.py b/src/registrar/tests/test_transition_domain_migrations.py
--- a/src/registrar/tests/test_transition_domain_migrations.py
+++ b/src/registrar/tests/test_transition_domain_migrations.py
@@ -18,7 +18,8 @@
 
 from registrar.models.contact import Contact
 
-from .common import MockEppLib, less_console_noise
+from .common import MockEppLib, MockSESClient, less_console_noise
+import boto3_mocking  # type: ignore
 
 
 class TestExtendExpirationDates(MockEppLib):
@@ -706,17 +707,21 @@ def run_transfer_domains(self):
     def run_master_script(self):
         # noqa here (E501) because splitting this up makes it
         # confusing to read.
-        with patch(
-            "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",  # noqa
-            return_value=True,
-        ):
-            call_command(
-                "master_domain_migrations",
-                runMigrations=True,
-                migrationDirectory=self.test_data_file_location,
-                migrationJSON=self.migration_json_filename,
-                disablePrompts=True,
-            )
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with patch(
+                "registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",  # noqa
+                return_value=True,
+            ):
+                with patch("registrar.utility.email.send_templated_email", return_value=None):
+                    call_command(
+                        "master_domain_migrations",
+                        runMigrations=True,
+                        migrationDirectory=self.test_data_file_location,
+                        migrationJSON=self.migration_json_filename,
+                        disablePrompts=True,
+                    )
+        print(f"here: {mock_client.EMAILS_SENT}")
 
     def compare_tables(
         self,
@@ -1019,6 +1024,7 @@ def test_logins(self):
             expected_missing_domain_invitations,
         )
 
+    @boto3_mocking.patching
     def test_send_domain_invitations_email(self):
         """Can send only a single domain invitation email."""
         with less_console_noise():
@@ -1027,9 +1033,12 @@ def test_send_domain_invitations_email(self):
 
         # this is one of the email addresses in data/test_contacts.txt
         output_stream = StringIO()
-        # also have to re-point the logging handlers to output_stream
-        with less_console_noise(output_stream):
-            call_command("send_domain_invitations", "testuser@gmail.com", stdout=output_stream)
+
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            # also have to re-point the logging handlers to output_stream
+            with less_console_noise(output_stream):
+                call_command("send_domain_invitations", "testuser@gmail.com", stdout=output_stream)
 
         # Check that we had the right numbers in our output
         output = output_stream.getvalue()
@@ -1037,6 +1046,7 @@ def test_send_domain_invitations_email(self):
         self.assertIn("Found 1 transition domains", output)
         self.assertTrue("would send email to testuser@gmail.com", output)
 
+    @boto3_mocking.patching
     def test_send_domain_invitations_two_emails(self):
         """Can send only a single domain invitation email."""
         with less_console_noise():
@@ -1045,11 +1055,14 @@ def test_send_domain_invitations_two_emails(self):
 
         # these are two email addresses in data/test_contacts.txt
         output_stream = StringIO()
-        # also have to re-point the logging handlers to output_stream
-        with less_console_noise(output_stream):
-            call_command(
-                "send_domain_invitations", "testuser@gmail.com", "agustina.wyman7@test.com", stdout=output_stream
-            )
+
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            # also have to re-point the logging handlers to output_stream
+            with less_console_noise(output_stream):
+                call_command(
+                    "send_domain_invitations", "testuser@gmail.com", "agustina.wyman7@test.com", stdout=output_stream
+                )
 
         # Check that we had the right numbers in our output
         output = output_stream.getvalue()
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -5,7 +5,7 @@
 from django.test import Client, TestCase
 from django.urls import reverse
 from django.contrib.auth import get_user_model
-from .common import MockEppLib, completed_application, create_user  # type: ignore
+from .common import MockEppLib, MockSESClient, completed_application, create_user  # type: ignore
 from django_webtest import WebTest  # type: ignore
 import boto3_mocking  # type: ignore
 
@@ -149,8 +149,11 @@ def test_application_multiple_applications_exist(self):
         """Test that an info message appears when user has multiple applications already"""
         # create and submit an application
         application = completed_application(user=self.user)
-        application.submit()
-        application.save()
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():
+                application.submit()
+                application.save()
 
         # now, attempt to create another one
         with less_console_noise():
@@ -1440,6 +1443,7 @@ def test_domain_user_add(self):
         response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
         self.assertContains(response, "Add a domain manager")
 
+    @boto3_mocking.patching
     def test_domain_user_add_form(self):
         """Adding an existing user works."""
         other_user, _ = get_user_model().objects.get_or_create(email="mayor@igorville.gov")
@@ -1449,7 +1453,11 @@ def test_domain_user_add_form(self):
         add_page.form["email"] = "mayor@igorville.gov"
 
         self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-        success_result = add_page.form.submit()
+
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():
+                success_result = add_page.form.submit()
 
         self.assertEqual(success_result.status_code, 302)
         self.assertEqual(
@@ -1478,7 +1486,12 @@ def test_domain_invitation_created(self):
         session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
         add_page.form["email"] = email_address
         self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-        success_result = add_page.form.submit()
+
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():
+                success_result = add_page.form.submit()
+
         self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
         success_page = success_result.follow()
 
@@ -1504,7 +1517,12 @@ def test_domain_invitation_created_for_caps_email(self):
         session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
         add_page.form["email"] = caps_email_address
         self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-        success_result = add_page.form.submit()
+
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():
+                success_result = add_page.form.submit()
+
         self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
         success_page = success_result.follow()
 
@@ -1524,11 +1542,12 @@ def test_domain_invitation_email_sent(self):
         mock_client = MagicMock()
         mock_client_instance = mock_client.return_value
         with boto3_mocking.clients.handler_for("sesv2", mock_client):
-            add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
-            session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
-            add_page.form["email"] = email_address
-            self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-            add_page.form.submit()
+            with less_console_noise():
+                add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
+                session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+                add_page.form["email"] = email_address
+                self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+                add_page.form.submit()
 
         # check the mock instance to see if `send_email` was called right
         mock_client_instance.send_email.assert_called_once_with(
@@ -1550,11 +1569,12 @@ def test_domain_invitation_email_has_email_as_requester_non_existent(self):
         mock_client_instance = mock_client.return_value
 
         with boto3_mocking.clients.handler_for("sesv2", mock_client):
-            add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
-            session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
-            add_page.form["email"] = email_address
-            self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-            add_page.form.submit()
+            with less_console_noise():
+                add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
+                session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+                add_page.form["email"] = email_address
+                self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+                add_page.form.submit()
 
         # check the mock instance to see if `send_email` was called right
         mock_client_instance.send_email.assert_called_once_with(
@@ -1588,11 +1608,12 @@ def test_domain_invitation_email_has_email_as_requester(self):
         mock_client_instance = mock_client.return_value
 
         with boto3_mocking.clients.handler_for("sesv2", mock_client):
-            add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
-            session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
-            add_page.form["email"] = email_address
-            self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-            add_page.form.submit()
+            with less_console_noise():
+                add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
+                session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+                add_page.form["email"] = email_address
+                self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+                add_page.form.submit()
 
         # check the mock instance to see if `send_email` was called right
         mock_client_instance.send_email.assert_called_once_with(
@@ -1630,11 +1651,12 @@ def test_domain_invitation_email_has_email_as_requester_staff(self):
         mock_client_instance = mock_client.return_value
 
         with boto3_mocking.clients.handler_for("sesv2", mock_client):
-            add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
-            session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
-            add_page.form["email"] = email_address
-            self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-            add_page.form.submit()
+            with less_console_noise():
+                add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
+                session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+                add_page.form["email"] = email_address
+                self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+                add_page.form.submit()
 
         # check the mock instance to see if `send_email` was called right
         mock_client_instance.send_email.assert_called_once_with(
@@ -1669,15 +1691,15 @@ def test_domain_invitation_email_displays_error_non_existent(self):
         self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
 
         mock_client = MagicMock()
-
         mock_error_message = MagicMock()
         with boto3_mocking.clients.handler_for("sesv2", mock_client):
             with patch("django.contrib.messages.error") as mock_error_message:
-                add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
-                session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
-                add_page.form["email"] = email_address
-                self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-                add_page.form.submit().follow()
+                with less_console_noise():
+                    add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
+                    session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+                    add_page.form["email"] = email_address
+                    self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+                    add_page.form.submit().follow()
 
         expected_message_content = "Can't send invitation email. No email is associated with your account."
 
@@ -1706,11 +1728,12 @@ def test_domain_invitation_email_displays_error(self):
         mock_error_message = MagicMock()
         with boto3_mocking.clients.handler_for("sesv2", mock_client):
             with patch("django.contrib.messages.error") as mock_error_message:
-                add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
-                session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
-                add_page.form["email"] = email_address
-                self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-                add_page.form.submit().follow()
+                with less_console_noise():
+                    add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
+                    session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
+                    add_page.form["email"] = email_address
+                    self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
+                    add_page.form.submit().follow()
 
         expected_message_content = "Can't send invitation email. No email is associated with your account."
 
@@ -1724,7 +1747,11 @@ def test_domain_invitation_cancel(self):
         """Posting to the delete view deletes an invitation."""
         email_address = "mayor@igorville.gov"
         invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
-        self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}))
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():
+                self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}))
+        mock_client.EMAILS_SENT.clear()
         with self.assertRaises(DomainInvitation.DoesNotExist):
             DomainInvitation.objects.get(id=invitation.id)
 
@@ -1736,8 +1763,11 @@ def test_domain_invitation_cancel_no_permissions(self):
         other_user = User()
         other_user.save()
         self.client.force_login(other_user)
-        with less_console_noise():  # permission denied makes console errors
-            result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}))
+        mock_client = MagicMock()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():  # permission denied makes console errors
+                result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}))
+
         self.assertEqual(result.status_code, 403)
 
     @boto3_mocking.patching
@@ -1753,7 +1783,11 @@ def test_domain_invitation_flow(self):
         session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
         add_page.form["email"] = email_address
         self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-        add_page.form.submit()
+
+        mock_client = MagicMock()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():
+                add_page.form.submit()
 
         # user was invited, create them
         new_user = User.objects.create(username=email_address, email=email_address)
@@ -1808,6 +1842,7 @@ def test_domain_nameservers_form_submit_subdomain_missing_ip(self):
         # attempt to submit the form without two hosts, both subdomains,
         # only one has ips
         nameservers_page.form["form-1-server"] = "ns2.igorville.gov"
+
         with less_console_noise():  # swallow log warning message
             result = nameservers_page.form.submit()
         # form submission was a post with an error, response should be a 200
@@ -2143,8 +2178,10 @@ def test_domain_security_email_form(self):
         session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
         security_email_page.form["security_email"] = "mayor@igorville.gov"
         self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
-        with less_console_noise():  # swallow log warning message
-            result = security_email_page.form.submit()
+        mock_client = MagicMock()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():  # swallow log warning message
+                result = security_email_page.form.submit()
         self.assertEqual(result.status_code, 302)
         self.assertEqual(
             result["Location"],
@@ -2490,9 +2527,12 @@ def test_application_withdraw(self):
         self.assertContains(detail_page, "Admin Tester")
         self.assertContains(detail_page, "Status:")
         # click the "Withdraw request" button
-        withdraw_page = detail_page.click("Withdraw request")
-        self.assertContains(withdraw_page, "Withdraw request for")
-        home_page = withdraw_page.click("Withdraw request")
+        mock_client = MockSESClient()
+        with boto3_mocking.clients.handler_for("sesv2", mock_client):
+            with less_console_noise():
+                withdraw_page = detail_page.click("Withdraw request")
+                self.assertContains(withdraw_page, "Withdraw request for")
+                home_page = withdraw_page.click("Withdraw request")
         # confirm that it has redirected, and the status has been updated to withdrawn
         self.assertRedirects(
             home_page,