diff --git a/src/registrar/management/commands/email_current_metadata_report.py b/src/registrar/management/commands/email_current_metadata_report.py index 31ddd67af..a8049ac37 100644 --- a/src/registrar/management/commands/email_current_metadata_report.py +++ b/src/registrar/management/commands/email_current_metadata_report.py @@ -9,7 +9,7 @@ from django.core.management import BaseCommand from django.conf import settings from registrar.utility import csv_export from io import StringIO -from ...utility.email import send_templated_email +from ...utility.email import send_templated_email, EmailSendingError logger = logging.getLogger(__name__) @@ -46,9 +46,11 @@ class Command(BaseCommand): logger.info("Generating report...") try: - self.email_current_metadata_report(zip_filename, email_to) + success = self.email_current_metadata_report(zip_filename, email_to) + if not success: + # TODO - #1317: Notify operations when auto report generation fails + raise EmailSendingError("Report was generated but failed to send via email.") except Exception as err: - # TODO - #1317: Notify operations when auto report generation fails raise err else: logger.info(f"Success! Created {zip_filename} and successfully sent out an email!") @@ -78,13 +80,24 @@ class Command(BaseCommand): encrypted_zip_in_bytes = self.get_encrypted_zip(zip_filename, reports, password) # Send the metadata file that is zipped - send_templated_email( - template_name="emails/metadata_body.txt", - subject_template_name="emails/metadata_subject.txt", - to_addresses=email_to, - context={"current_date_str": datetime.now().strftime("%Y-%m-%d")}, - attachment_file=encrypted_zip_in_bytes, - ) + try: + send_templated_email( + template_name="emails/metadata_body.txt", + subject_template_name="emails/metadata_subject.txt", + to_addresses=email_to, + context={"current_date_str": datetime.now().strftime("%Y-%m-%d")}, + attachment_file=encrypted_zip_in_bytes, + ) + return True + except EmailSendingError as err: + logger.error( + "Failed to send metadata email:\n" + f" Subject: metadata_subject.txt\n" + f" To: {email_to}\n" + f" Error: {err}", + exc_info=True, + ) + return False def get_encrypted_zip(self, zip_filename, reports, password): """Helper function for encrypting the attachment file""" diff --git a/src/registrar/management/commands/send_domain_invitations.py b/src/registrar/management/commands/send_domain_invitations.py index 588e2881b..6b2aa9184 100644 --- a/src/registrar/management/commands/send_domain_invitations.py +++ b/src/registrar/management/commands/send_domain_invitations.py @@ -136,9 +136,12 @@ class Command(BaseCommand): ) except EmailSendingError as err: logger.error( - f"email did not send successfully to {email_data['email']} " - f"for {[domain for domain in email_data['domains']]}" - f": {err}" + "Failed to send transition domain invitation email:\n" + f" Subjec template: transition_domain_invitation_subject.txt\n" + f" To: {email_data['email']}\n" + f" Domains: {', '.join(email_data['domains'])}\n" + f" Error: {err}", + exc_info=True, ) # if email failed to send, set error in domains_with_errors for each # domain in the email so that transition domain email_sent is not set diff --git a/src/registrar/management/commands/send_expiring_soon_domains_notification.py b/src/registrar/management/commands/send_expiring_soon_domains_notification.py index dc3183c1e..06542570c 100644 --- a/src/registrar/management/commands/send_expiring_soon_domains_notification.py +++ b/src/registrar/management/commands/send_expiring_soon_domains_notification.py @@ -97,9 +97,17 @@ class Command(BaseCommand): context=context, ) logger.info(f"Sent email for domain {domain.name} to managers and CC’d org admins") - except EmailSendingError as e: + except EmailSendingError as err: if not dryrun: - logger.warning(f"Failed to send email for domain {domain.name}. Reason: {e}") + logger.error( + "Failed to send expiring soon email(s):\n" + f" Subject template: {subject_template}\n" + f" To: {', '.join(domain_manager_emails)}\n" + f" CC: {', '.join(portfolio_admin_emails)}\n" + f" Domain: {domain.name}\n" + f" Error: {err}", + exc_info=True, + ) all_emails_sent = False if all_emails_sent: diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 801242fae..6a981321a 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1032,8 +1032,17 @@ class DomainRequest(TimeStampedModel): wrap_email=wrap_email, ) logger.info(f"The {new_status} email sent to: {recipient.email}") - except EmailSendingError: - logger.warning("Failed to send confirmation email", exc_info=True) + except EmailSendingError as err: + logger.error( + "Failed to send status update to creator email:\n" + f" Type: {new_status}\n" + f" Subject template: {email_template_subject}\n" + f" To: {recipient.email}\n" + f" CC: {', '.join(cc_addresses)}\n" + f" BCC: {bcc_address}" + f" Error: {err}", + exc_info=True, + ) def investigator_exists_and_is_staff(self): """Checks if the current investigator is in a valid state for a state transition""" diff --git a/src/registrar/tests/test_email_invitations.py b/src/registrar/tests/test_email_invitations.py index f546cbdad..016fac4af 100644 --- a/src/registrar/tests/test_email_invitations.py +++ b/src/registrar/tests/test_email_invitations.py @@ -940,18 +940,22 @@ class TestSendPortfolioMemberPermissionUpdateEmail(unittest.TestCase): permissions.user.email = "user@example.com" permissions.portfolio.organization_name = "Test Portfolio" - mock_get_requestor_email.return_value = "requestor@example.com" + mock_get_requestor_email.return_value = MagicMock(name="mock.email") # Call function result = send_portfolio_member_permission_update_email(requestor, permissions) # Assertions - mock_logger.warning.assert_called_once_with( - "Could not send email organization member update notification to %s for portfolio: %s", - permissions.user.email, - permissions.portfolio.organization_name, - exc_info=True, + expected_message = ( + "Failed to send organization member update notification email:\n" + f" Requestor Email: {mock_get_requestor_email.return_value}\n" + f" Subject template: portfolio_update_subject.txt\n" + f" To: {permissions.user.email}\n" + f" Portfolio: {permissions.portfolio}\n" + f" Error: Email failed" ) + + mock_logger.error.assert_called_once_with(expected_message, exc_info=True) self.assertFalse(result) @patch("registrar.utility.email_invitations._get_requestor_email", side_effect=Exception("Unexpected error")) @@ -1013,18 +1017,22 @@ class TestSendPortfolioMemberPermissionRemoveEmail(unittest.TestCase): permissions.user.email = "user@example.com" permissions.portfolio.organization_name = "Test Portfolio" - mock_get_requestor_email.return_value = "requestor@example.com" + mock_get_requestor_email.return_value = MagicMock(name="mock.email") # Call function result = send_portfolio_member_permission_remove_email(requestor, permissions) # Assertions - mock_logger.warning.assert_called_once_with( - "Could not send email organization member removal notification to %s for portfolio: %s", - permissions.user.email, - permissions.portfolio.organization_name, - exc_info=True, + expected_message = ( + "Failed to send portfolio member removal email:\n" + f" Requestor Email: {mock_get_requestor_email.return_value}\n" + f" Subject template: portfolio_removal_subject.txt\n" + f" To: {permissions.user.email}\n" + f" Portfolio: {permissions.portfolio}\n" + f" Error: Email failed" ) + + mock_logger.error.assert_called_once_with(expected_message, exc_info=True) self.assertFalse(result) @patch("registrar.utility.email_invitations._get_requestor_email", side_effect=Exception("Unexpected error")) @@ -1092,10 +1100,12 @@ class TestSendPortfolioInvitationRemoveEmail(unittest.TestCase): result = send_portfolio_invitation_remove_email(requestor, invitation) # Assertions - mock_logger.warning.assert_called_once_with( - "Could not send email organization member removal notification to %s for portfolio: %s", - invitation.email, - invitation.portfolio.organization_name, + mock_logger.error.assert_called_once_with( + "Failed to send portfolio invitation removal email:\n" + f" Subject template: portfolio_removal_subject.txt\n" + f" To: {invitation.email}\n" + f" Portfolio: {invitation.portfolio.organization_name}\n" + f" Error: Email failed", exc_info=True, ) self.assertFalse(result) diff --git a/src/registrar/utility/email_invitations.py b/src/registrar/utility/email_invitations.py index ca246df34..c9b7ccaa8 100644 --- a/src/registrar/utility/email_invitations.py +++ b/src/registrar/utility/email_invitations.py @@ -98,6 +98,15 @@ def _send_domain_invitation_email(email, requestor_email, domains, requested_use ) except EmailSendingError as err: domain_names = ", ".join([domain.name for domain in domains]) + logger.error( + "Failed to send domain invitation email:\n" + f" Requestor Email: {requestor_email}\n" + f" Subject template: domain_invitation_subject.txt\n" + f" To: {email}\n" + f" Domains: {domain_names}\n" + f" Error: {err}", + exc_info=True, + ) raise EmailSendingError(f"Could not send email invitation to {email} for domains: {domain_names}") from err @@ -173,9 +182,15 @@ def _send_domain_invitation_update_emails_to_domain_managers( "date": date.today(), }, ) - except EmailSendingError: - logger.warning( - f"Could not send email manager notification to {user.email} for domain: {domain.name}", exc_info=True + except EmailSendingError as err: + logger.error( + "Failed to send domain manager update notification email:\n" + f" Requestor Email: {requestor_email}\n" + f" Subject: domain_manager_notification_subject.txt\n" + f" To: {user.email}\n" + f" Domain: {domain.name}\n" + f" Error: {err}", + exc_info=True, ) all_emails_sent = False return all_emails_sent @@ -220,11 +235,15 @@ def send_domain_manager_removal_emails_to_domain_managers( "date": date.today(), }, ) - except EmailSendingError: - logger.warning( - "Could not send notification email to %s for domain %s", - user.email, - domain.name, + except EmailSendingError as err: + logger.error( + "Failed to send domain manager deleted notification email:\n" + f" User that did the removing: {removed_by_user}\n" + f" Domain manager removed: {manager_removed_email}\n" + f" Subject template: domain_manager_deleted_notification_subject.txt\n" + f" To: {user.email}\n" + f" Domain: {domain.name}\n" + f" Error: {err}", exc_info=True, ) all_emails_sent = False @@ -265,6 +284,15 @@ def send_portfolio_invitation_email(email: str, requestor, portfolio, is_admin_i }, ) except EmailSendingError as err: + logger.error( + "Failed to send portfolio invitation email:\n" + f" Requestor Email: {requestor_email}\n" + f" Subject template: portfolio_invitation_subject.txt\n" + f" To: {email}\n" + f" Portfolio: {portfolio}\n" + f" Error: {err}", + exc_info=True, + ) raise EmailSendingError( f"Could not sent email invitation to {email} for portfolio {portfolio}. Portfolio invitation not saved." ) from err @@ -319,11 +347,14 @@ def send_portfolio_update_emails_to_portfolio_admins(editor, portfolio, updated_ "updated_info": updated_page, }, ) - except EmailSendingError: - logger.warning( - "Could not send email organization admin notification to %s " "for portfolio: %s", - user.email, - portfolio, + except EmailSendingError as err: + logger.error( + "Failed to send portfolio org update notification email:\n" + f" Requested User: {user}\n" + f" Subject template: portfolio_org_update_notification_subject.txt\n" + f" To: {user.email}\n" + f" Portfolio: {portfolio}\n" + f" Error: {err}", exc_info=True, ) all_emails_sent = False @@ -362,11 +393,14 @@ def send_portfolio_member_permission_update_email(requestor, permissions: UserPo "date": date.today(), }, ) - except EmailSendingError: - logger.warning( - "Could not send email organization member update notification to %s " "for portfolio: %s", - permissions.user.email, - permissions.portfolio.organization_name, + except EmailSendingError as err: + logger.error( + "Failed to send organization member update notification email:\n" + f" Requestor Email: {requestor_email}\n" + f" Subject template: portfolio_update_subject.txt\n" + f" To: {permissions.user.email}\n" + f" Portfolio: {permissions.portfolio}\n" + f" Error: {err}", exc_info=True, ) return False @@ -403,11 +437,14 @@ def send_portfolio_member_permission_remove_email(requestor, permissions: UserPo "requestor_email": requestor_email, }, ) - except EmailSendingError: - logger.warning( - "Could not send email organization member removal notification to %s " "for portfolio: %s", - permissions.user.email, - permissions.portfolio.organization_name, + except EmailSendingError as err: + logger.error( + "Failed to send portfolio member removal email:\n" + f" Requestor Email: {requestor_email}\n" + f" Subject template: portfolio_removal_subject.txt\n" + f" To: {permissions.user.email}\n" + f" Portfolio: {permissions.portfolio}\n" + f" Error: {err}", exc_info=True, ) return False @@ -444,11 +481,13 @@ def send_portfolio_invitation_remove_email(requestor, invitation: PortfolioInvit "requestor_email": requestor_email, }, ) - except EmailSendingError: - logger.warning( - "Could not send email organization member removal notification to %s " "for portfolio: %s", - invitation.email, - invitation.portfolio.organization_name, + except EmailSendingError as err: + logger.error( + "Failed to send portfolio invitation removal email:\n" + f" Subject template: portfolio_removal_subject.txt\n" + f" To: {invitation.email}\n" + f" Portfolio: {invitation.portfolio.organization_name}\n" + f" Error: {err}", exc_info=True, ) return False @@ -497,11 +536,15 @@ def _send_portfolio_admin_addition_emails_to_portfolio_admins(email: str, reques "date": date.today(), }, ) - except EmailSendingError: - logger.warning( - "Could not send email organization admin notification to %s " "for portfolio: %s", - user.email, - portfolio.organization_name, + except EmailSendingError as err: + logger.error( + "Failed to send portfolio admin addition notification email:\n" + f" Requestor Email: {requestor_email}\n" + f" Subject template: portfolio_admin_addition_notification_subject.txt\n" + f" To: {user.email}\n" + f" Portfolio: {portfolio}\n" + f" Portfolio Admin: {user}\n" + f" Error: {err}", exc_info=True, ) all_emails_sent = False @@ -550,11 +593,14 @@ def _send_portfolio_admin_removal_emails_to_portfolio_admins(email: str, request "date": date.today(), }, ) - except EmailSendingError: - logger.warning( - "Could not send email organization admin notification to %s " "for portfolio: %s", - user.email, - portfolio.organization_name, + except EmailSendingError as err: + logger.error( + "Failed to send portfolio admin removal notification email:\n" + f" Requestor Email: {requestor_email}\n" + f" Subject template: portfolio_admin_removal_notification_subject.txt\n" + f" To: {user.email}\n" + f" Portfolio: {portfolio.organization_name}\n" + f" Error: {err}", exc_info=True, ) all_emails_sent = False diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b9daf15f9..330142926 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -375,11 +375,13 @@ class DomainFormBaseView(DomainBaseView, FormMixin): context["recipient"] = manager try: send_templated_email(template, subject_template, to_addresses=[manager.email], context=context) - except EmailSendingError: - logger.warning( - "Could not send notification email to %s for domain %s", - manager.email, - domain.name, + except EmailSendingError as err: + logger.error( + "Failed to send notification email:\n" + f" Subject template: {subject_template}\n" + f" To: {manager.email}\n" + f" Domain: {domain.name}\n" + f" Error: {err}", exc_info=True, ) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 3585e17fc..13bdcad73 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -1018,8 +1018,14 @@ class Review(DomainRequestWizard): context=context, ) logger.info("A submission confirmation email was sent to ombdotgov@omb.eop.gov") - except EmailSendingError: - logger.warning("Failed to send confirmation email", exc_info=True) + except EmailSendingError as err: + logger.error( + "Failed to send OMB submission confirmation email:\n" + f" Subject template: omb_submission_confirmation_subject.txt\n" + f" To: ombdotgov@omb.eop.gov\n" + f" Error: {err}", + exc_info=True, + ) class Finished(DomainRequestWizard):