Skip to content

Commit

Permalink
MPP-3119: wip complaint notification disables mask
Browse files Browse the repository at this point in the history
  • Loading branch information
groovecoder committed Sep 24, 2024
1 parent 2e57616 commit 156a949
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 11 deletions.
47 changes: 41 additions & 6 deletions emails/tests/views_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,12 @@ def test_sns_message_with_hard_bounce_and_optout(self) -> None:

@override_settings(STATSD_ENABLED=True)
class ComplaintHandlingTest(TestCase):
"""Test Complaint notifications and events."""
"""
Test Complaint notifications and events.
Example derived from:
https://docs.aws.amazon.com/ses/latest/dg/notification-contents.html#complaint-object
"""

def setUp(self):
self.user = baker.make(User, email="[email protected]")
Expand All @@ -1076,11 +1081,11 @@ def setUp(self):

def test_notification_type_complaint(self):
"""
A notificationType of complaint increments a counter, logs details, and
returns 200.
Example derived from:
https://docs.aws.amazon.com/ses/latest/dg/notification-contents.html#complaint-object
A notificationType of complaint:
1. increments a counter
2. logs details,
3. sets the user profile's auto_block_spam = True, and
4. returns 200.
"""
assert self.user.profile.auto_block_spam is False

Expand Down Expand Up @@ -1125,6 +1130,36 @@ def test_complaint_log_with_optout(self) -> None:
assert log_data["user_match"] == "found"
assert not log_data["fxa_id"]

def test_complaint_disables_mask(self):
"""
A notificationType of complaint:
1. sets enabled=False on the mask, and
2. returns 200.
"""
self.ra = baker.make(
RelayAddress, user=self.user, address="ebsbdsan7", domain=2
)

# The top-level JSON object for complaints includes a "mail" field
# which contains information about the original mail to which the notification
# pertains. So, add a "mail" field with content from our russian_spam fixture
russian_spam_notification = create_notification_from_email(
EMAIL_INCOMING["russian_spam"]
)
spam_mail_content = json.loads(
russian_spam_notification.get("Message", "")
).get("mail", {})
complaint_body_message = json.loads(self.complaint_body["Message"])
complaint_body_message["mail"] = spam_mail_content
complaint_body_with_spam_mail = {"Message": json.dumps(complaint_body_message)}
assert self.ra.enabled is True

response = _sns_notification(complaint_body_with_spam_mail)
assert response.status_code == 200

self.ra.refresh_from_db()
assert self.ra.enabled is False


class SNSNotificationRemoveEmailsInS3Test(TestCase):
def setUp(self) -> None:
Expand Down
37 changes: 32 additions & 5 deletions emails/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1368,7 +1368,9 @@ def _handle_reply(
return HttpResponse("Sent email to final recipient.", status=200)


def _get_domain_address(local_portion: str, domain_portion: str) -> DomainAddress:
def _get_domain_address(
local_portion: str, domain_portion: str, create: bool = True
) -> DomainAddress:
"""
Find or create the DomainAddress for the parts of an email address.
Expand Down Expand Up @@ -1396,6 +1398,8 @@ def _get_domain_address(local_portion: str, domain_portion: str) -> DomainAddres
user=locked_profile.user, address=local_portion, domain=domain_numerical
).first()
if domain_address is None:
if not create:
raise ObjectDoesNotExist("Address does not exist")
# TODO: Consider flows when a user generating alias on a fly
# was unable to receive an email due to user no longer being a
# premium user as seen in exception thrown on make_domain_address
Expand All @@ -1414,12 +1418,12 @@ def _get_domain_address(local_portion: str, domain_portion: str) -> DomainAddres
raise e


def _get_address(address: str) -> RelayAddress | DomainAddress:
def _get_address(address: str, create: bool = True) -> RelayAddress | DomainAddress:
"""
Find or create the RelayAddress or DomainAddress for an email address.
If an unknown email address is for a valid subdomain, a new DomainAddress
will be created.
If an unknown email address is for a valid subdomain, and create is True,
a new DomainAddress will be created.
On failure, raises exception based on Django's ObjectDoesNotExist:
* RelayAddress.DoesNotExist - looks like RelayAddress, deleted or does not exist
Expand All @@ -1445,6 +1449,8 @@ def _get_address(address: str) -> RelayAddress | DomainAddress:
)
return relay_address
except RelayAddress.DoesNotExist as e:
if not create:
raise e
try:
DeletedAddress.objects.get(
address_hash=address_hash(local_address, domain=domain)
Expand Down Expand Up @@ -1572,6 +1578,11 @@ def _handle_complaint(message_json: AWS_SNSMessageJSON) -> HttpResponse:
"""
Handle an AWS SES complaint notification.
Sets the user's auto_block_spam flag to True.
Disables the mask thru which the spam mail was forwarded, and sends an email to the
user to notify them the mask is disabled and can be re-enabled on their dashboard.
For more information, see:
https://docs.aws.amazon.com/ses/latest/dg/notification-contents.html#complaint-object
Expand All @@ -1580,7 +1591,7 @@ def _handle_complaint(message_json: AWS_SNSMessageJSON) -> HttpResponse:
* 200 response if all match or none are given
Emits a counter metric "email_complaint" with these tags:
* complaint_subtype: 'onaccounsuppressionlist', or 'none' if omitted
* complaint_subtype: 'onaccountsuppressionlist', or 'none' if omitted
* complaint_feedback - feedback enumeration from ISP or 'none'
* user_match: 'found', 'missing', error states 'no_address' and 'no_recipients'
* relay_action: 'no_action', 'auto_block_spam'
Expand Down Expand Up @@ -1634,6 +1645,22 @@ def _handle_complaint(message_json: AWS_SNSMessageJSON) -> HttpResponse:
profile.auto_block_spam = True
profile.save()

for destination_address in message_json.get("mail", {}).get("destination", []):
try:
address = _get_address(destination_address, False)
address.enabled = False
address.save()
# TODO: email the user that we disabled the mask
except (
ObjectDoesNotExist,
RelayAddress.DoesNotExist,
DomainAddress.DoesNotExist,
):
logger.error(
"Received a complaint from a destination address that does not match "
"a Relay address.",
)

if not complaint_data:
# Data when there are no identified recipients
complaint_data = [{"user_match": "no_recipients", "relay_action": "no_action"}]
Expand Down

0 comments on commit 156a949

Please sign in to comment.