Repository: cisagov/manage.get.gov

Issue:
Email for "You've been added to a .gov domain" doesn't name the person who initiated the invitation
### Current Behavior

When a new domain manager is added, users receive an email that looks odd and reads suspiciously, like something is missing. It doesn't actually say who initiated the invitation:

![image](https://github.com/cisagov/manage.get.gov/assets/603901/117d673d-1713-4196-ad37-afe5548ba1a8)


### Expected Behavior

When a non-CISA-admin domain manager adds a new domain manager, the email text should not try to add a first/last name of the creator and should instead show the email of whoever clicked the add user button (no check on creator)

When a cisa-admin adds a new domain manager, then the email shown should be help@get.gov. (this includes anyone with is_staff access)


### Steps to Reproduce

1. Add a new domain manager at an email address you control.
2. See that the email text is wrong.


### Environment

Stable: manage.get.gov

### Additional Context

[Slack thread](https://cisa-corp.slack.com/archives/C05BGB4L5NF/p1701893276703299?thread_ts=1701892663.512319&cid=C05BGB4L5NF)

> Found the bug, it is set to print the creator of the domain, not the person who sent the invitation. And ALL transition domains have no creator first name/ last name



### Issue Links

_No response_


Discussion:
Would like this to say "requestor's email" has invited you to manage.... which is reliable and recognizable to the recipient.

Resolving patch:
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
@@ -17,7 +17,6 @@
 
 from registrar.models import (
     Domain,
-    DomainInformation,
     DomainInvitation,
     User,
     UserDomainRole,
@@ -644,21 +643,27 @@ def _domain_abs_url(self):
         """Get an absolute URL for this domain."""
         return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
 
-    def _send_domain_invitation_email(self, email: str, add_success=True):
+    def _send_domain_invitation_email(self, email: str, requester: User, add_success=True):
         """Performs the sending of the domain invitation email,
         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"""
-        # created a new invitation in the database, so send an email
-        domainInfoResults = DomainInformation.objects.filter(domain=self.object)
-        domainInfo = domainInfoResults.first()
-        first = ""
-        last = ""
-        if domainInfo is not None:
-            first = domainInfo.creator.first_name
-            last = domainInfo.creator.last_name
-        full_name = f"{first} {last}"
+
+        # Set a default email address to send to for staff
+        requester_email = "help@get.gov"
+
+        # Check if the email requester has a valid email address
+        if not requester.is_staff and requester.email is not None and requester.email.strip() != "":
+            requester_email = requester.email
+        elif not requester.is_staff:
+            messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
+            logger.error(
+                f"Can't send email to '{email}' on domain '{self.object}'."
+                f"No email exists for the requester '{requester.username}'.",
+                exc_info=True,
+            )
+            return None
 
         try:
             send_templated_email(
@@ -668,7 +673,7 @@ def _send_domain_invitation_email(self, email: str, add_success=True):
                 context={
                     "domain_url": self._domain_abs_url(),
                     "domain": self.object,
-                    "full_name": full_name,
+                    "requester_email": requester_email,
                 },
             )
         except EmailSendingError:
@@ -683,7 +688,7 @@ def _send_domain_invitation_email(self, email: str, add_success=True):
             if add_success:
                 messages.success(self.request, f"Invited {email} to this domain.")
 
-    def _make_invitation(self, email_address: str):
+    def _make_invitation(self, email_address: str, requester: 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:
@@ -693,21 +698,22 @@ def _make_invitation(self, email_address: str):
                 f"{email_address} has already been invited to this domain.",
             )
         else:
-            self._send_domain_invitation_email(email=email_address)
+            self._send_domain_invitation_email(email=email_address, requester=requester)
         return redirect(self.get_success_url())
 
     def form_valid(self, form):
         """Add the specified user on this domain."""
         requested_email = form.cleaned_data["email"]
+        requester = self.request.user
         # look up a user with that email
         try:
             requested_user = User.objects.get(email=requested_email)
         except User.DoesNotExist:
             # no matching user, go make an invitation
-            return self._make_invitation(requested_email)
+            return self._make_invitation(requested_email, requester)
         else:
             # if user already exists then just send an email
-            self._send_domain_invitation_email(requested_email, add_success=False)
+            self._send_domain_invitation_email(requested_email, requester, add_success=False)
 
         try:
             UserDomainRole.objects.create(


Test patch:
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
@@ -6,7 +6,6 @@
 from django.urls import reverse
 from django.contrib.auth import get_user_model
 from .common import MockEppLib, completed_application, create_user  # type: ignore
-
 from django_webtest import WebTest  # type: ignore
 import boto3_mocking  # type: ignore
 
@@ -1331,6 +1330,12 @@ def test_domain_detail_with_no_information_or_application(self):
 
 
 class TestDomainManagers(TestDomainOverview):
+    def tearDown(self):
+        """Ensure that the user has its original permissions"""
+        super().tearDown()
+        self.user.is_staff = False
+        self.user.save()
+
     def test_domain_managers(self):
         response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id}))
         self.assertContains(response, "Domain managers")
@@ -1434,6 +1439,113 @@ def test_domain_invitation_email_sent(self):
             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(
+            FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
+            Destination={"ToAddresses": [email_address]},
+            Content=ANY,
+        )
+
+    @boto3_mocking.patching
+    def test_domain_invitation_email_has_email_as_requester_non_existent(self):
+        """Inviting a non existent user sends them an email, with email as the name."""
+        # make sure there is no user with this email
+        email_address = "mayor@igorville.gov"
+        User.objects.filter(email=email_address).delete()
+
+        self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
+
+        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()
+
+        # check the mock instance to see if `send_email` was called right
+        mock_client_instance.send_email.assert_called_once_with(
+            FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
+            Destination={"ToAddresses": [email_address]},
+            Content=ANY,
+        )
+
+        # Check the arguments passed to send_email method
+        _, kwargs = mock_client_instance.send_email.call_args
+
+        # Extract the email content, and check that the message is as we expect
+        email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
+        self.assertIn("info@example.com", email_content)
+
+        # Check that the requesters first/last name do not exist
+        self.assertNotIn("First", email_content)
+        self.assertNotIn("Last", email_content)
+        self.assertNotIn("First Last", email_content)
+
+    @boto3_mocking.patching
+    def test_domain_invitation_email_has_email_as_requester(self):
+        """Inviting a user sends them an email, with email as the name."""
+        # Create a fake user object
+        email_address = "mayor@igorville.gov"
+        User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
+
+        self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
+
+        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()
+
+        # check the mock instance to see if `send_email` was called right
+        mock_client_instance.send_email.assert_called_once_with(
+            FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
+            Destination={"ToAddresses": [email_address]},
+            Content=ANY,
+        )
+
+        # Check the arguments passed to send_email method
+        _, kwargs = mock_client_instance.send_email.call_args
+
+        # Extract the email content, and check that the message is as we expect
+        email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
+        self.assertIn("info@example.com", email_content)
+
+        # Check that the requesters first/last name do not exist
+        self.assertNotIn("First", email_content)
+        self.assertNotIn("Last", email_content)
+        self.assertNotIn("First Last", email_content)
+
+    @boto3_mocking.patching
+    def test_domain_invitation_email_has_email_as_requester_staff(self):
+        """Inviting a user sends them an email, with email as the name."""
+        # Create a fake user object
+        email_address = "mayor@igorville.gov"
+        User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
+
+        # Make sure the user is staff
+        self.user.is_staff = True
+        self.user.save()
+
+        self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
+
+        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()
+
         # check the mock instance to see if `send_email` was called right
         mock_client_instance.send_email.assert_called_once_with(
             FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
@@ -1441,6 +1553,83 @@ def test_domain_invitation_email_sent(self):
             Content=ANY,
         )
 
+        # Check the arguments passed to send_email method
+        _, kwargs = mock_client_instance.send_email.call_args
+
+        # Extract the email content, and check that the message is as we expect
+        email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
+        self.assertIn("help@get.gov", email_content)
+
+        # Check that the requesters first/last name do not exist
+        self.assertNotIn("First", email_content)
+        self.assertNotIn("Last", email_content)
+        self.assertNotIn("First Last", email_content)
+
+    @boto3_mocking.patching
+    def test_domain_invitation_email_displays_error_non_existent(self):
+        """Inviting a non existent user sends them an email, with email as the name."""
+        # make sure there is no user with this email
+        email_address = "mayor@igorville.gov"
+        User.objects.filter(email=email_address).delete()
+
+        # Give the user who is sending the email an invalid email address
+        self.user.email = ""
+        self.user.save()
+
+        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()
+
+        expected_message_content = "Can't send invitation email. No email is associated with your account."
+
+        # Grab the message content
+        returned_error_message = mock_error_message.call_args[0][1]
+
+        # Check that the message content is what we expect
+        self.assertEqual(expected_message_content, returned_error_message)
+
+    @boto3_mocking.patching
+    def test_domain_invitation_email_displays_error(self):
+        """When the requesting user has no email, an error is displayed"""
+        # make sure there is no user with this email
+        # Create a fake user object
+        email_address = "mayor@igorville.gov"
+        User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com")
+
+        # Give the user who is sending the email an invalid email address
+        self.user.email = ""
+        self.user.save()
+
+        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()
+
+        expected_message_content = "Can't send invitation email. No email is associated with your account."
+
+        # Grab the message content
+        returned_error_message = mock_error_message.call_args[0][1]
+
+        # Check that the message content is what we expect
+        self.assertEqual(expected_message_content, returned_error_message)
+
     def test_domain_invitation_cancel(self):
         """Posting to the delete view deletes an invitation."""
         email_address = "mayor@igorville.gov"