Repository: cisagov/manage.get.gov

Issue:
Don't add user to a domain invitation if we don't have a connection to SES (mail infra)
### Issue description

If there's an issue with our connection to SES, we shouldn't add a user to a domain invitation. However, right now users get added. (See screenshot on #1779)

[Slack thread ](https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1707929221903979)of discussion

### Acceptance criteria

- [ ] Add a user to the domain invitation table only after confirming the connection to SES is successful

### Additional context

This issue doesn't attempt to solve the situation where a user types in an email address to a non-existent mailbox or some other SMTP failure (which was part of the original issue). 

Right now we add a new DomainInvitation object before trying to send the email (note the try except block in the code). This ticket is just to move the DomainInvitation creation to after we try to send it. You could use an "else" block in the try/except or you could just move the domain invitation below the code line that actually sends the email and is within the try block.

### Links to other issues

Related: #1779


Discussion:
refinement question: Should we change the domain invitation status to Failed to Send or something? That way if an analyst is dealing with a help desk question about stuff not sending they would be able to look and see clearly, "yep it hit an error" or "no it should have been sending, maybe the email is wrong". I don't think this is a very common thing since we are using aws email sending which is pretty reliable. CC: @Katherine-Osos @h-m-f-t 
@abroddrick Yes, I think we should indicate that with messaging. This related design ticket should help nail down how that will be presented to the user: #1779 
Making new states sounds about right, @abroddrick. It does seem like the likelier failure mode is sending to an bad email address that throws an SMTP 550 or something. Can we get SMTP errors from SES?
Per request from PO, added to refinement fireside meeting on 3/14.
In product huddle, we determined that this issue should be revised to focus just on the SES infrastructure connection. Note the comment in additional context, which might be future work.

Resolving patch:
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
@@ -32,8 +32,9 @@ def send_templated_email(
     template_name and subject_template_name are relative to the same template
     context as Django's HTML templates. context gives additional information
     that the template may use.
+
+    Raises EmailSendingError if SES client could not be accessed
     """
-    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)
 
@@ -48,7 +49,9 @@ def send_templated_email(
             aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
             config=settings.BOTO_CONFIG,
         )
+        logger.info(f"An email was sent! Template name: {template_name} to {to_address}")
     except Exception as exc:
+        logger.debug("E-mail unable to send! Could not access the SES client.")
         raise EmailSendingError("Could not access the SES client.") from exc
 
     destination = {"ToAddresses": [to_address]}
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -734,7 +734,10 @@ def _send_domain_invitation_email(self, email: str, requestor: User, add_success
         does not make a domain information object
         email: string- email to send to
         add_success: bool- default True indicates:
-          adding a success message to the view if the email sending succeeds"""
+        adding a success message to the view if the email sending succeeds
+
+        raises EmailSendingError
+        """
 
         # Set a default email address to send to for staff
         requestor_email = settings.DEFAULT_FROM_EMAIL
@@ -762,33 +765,43 @@ def _send_domain_invitation_email(self, email: str, requestor: User, add_success
                     "requestor_email": requestor_email,
                 },
             )
-        except EmailSendingError:
-            messages.warning(self.request, "Could not send email invitation.")
+        except EmailSendingError as exc:
             logger.warn(
                 "Could not sent email invitation to %s for domain %s",
                 email,
                 self.object,
                 exc_info=True,
             )
+            raise EmailSendingError("Could not send email invitation.") from exc
         else:
             if add_success:
                 messages.success(self.request, f"{email} has been invited to this domain.")
 
     def _make_invitation(self, email_address: str, requestor: User):
         """Make a Domain invitation for this email and redirect with a message."""
-        invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
-        if not created:
+        # Check to see if an invite has already been sent (NOTE: we do not want to create an invite just yet.)
+        try:
+            invite = DomainInvitation.objects.get(email=email_address, domain=self.object)
             # that invitation already existed
-            messages.warning(
-                self.request,
-                f"{email_address} has already been invited to this domain.",
-            )
-        else:
-            self._send_domain_invitation_email(email=email_address, requestor=requestor)
+            if invite is not None:
+                messages.warning(
+                    self.request,
+                    f"{email_address} has already been invited to this domain.",
+                )
+        except DomainInvitation.DoesNotExist:
+            # Try to send the invitation.  If it succeeds, add it to the DomainInvitation table.
+            try:
+                self._send_domain_invitation_email(email=email_address, requestor=requestor)
+            except EmailSendingError:
+                messages.warning(self.request, "Could not send email invitation.")
+            else:
+                # (NOTE: only create a domainInvitation if the e-mail sends correctly)
+                DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
         return redirect(self.get_success_url())
 
     def form_valid(self, form):
-        """Add the specified user on this domain."""
+        """Add the specified user on this domain.
+        Throws EmailSendingError."""
         requested_email = form.cleaned_data["email"]
         requestor = self.request.user
         # look up a user with that email
@@ -799,7 +812,22 @@ def form_valid(self, form):
             return self._make_invitation(requested_email, requestor)
         else:
             # if user already exists then just send an email
-            self._send_domain_invitation_email(requested_email, requestor, add_success=False)
+            try:
+                self._send_domain_invitation_email(requested_email, requestor, add_success=False)
+            except EmailSendingError:
+                logger.warn(
+                    "Could not send email invitation (EmailSendingError)",
+                    self.object,
+                    exc_info=True,
+                )
+                messages.warning(self.request, "Could not send email invitation.")
+            except Exception:
+                logger.warn(
+                    "Could not send email invitation (Other Exception)",
+                    self.object,
+                    exc_info=True,
+                )
+                messages.warning(self.request, "Could not send email invitation.")
 
         try:
             UserDomainRole.objects.create(


Test patch: