-
Notifications
You must be signed in to change notification settings - Fork 182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MPP-3932: Disable masks on complaints, round 2 #5115
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whoa this is big for just 3 files. You should probably skim emails/tests/views_tests.py
, and focus on emails/views.py
. Here's some comments that might help.
@@ -58,7 +58,6 @@ module = [ | |||
"django_ftl", | |||
"django_ftl.bundles", | |||
"google.*", | |||
"google_measurement_protocol", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the only line I meant to change in here. The rest were done by my editor's TOML formatter.
There are indeed 2 complaints for each time the simulator is used on dev. For one pair, the first has a
I'm going to see if there is a similar pattern in production complaints. If not, this may be a dev misconfiguration, or a quirk of the simulator. It looks like we might want to ignore either |
No, production does not have this pattern. Digging in, there are two ways to monitor complaints:
We have both configured on the development server. Looking at my notes, it is possible I enabled this, as I was looking into complaint formats in May 2022. I turned off email feedback forwarding of complaints for our dev domain identity. This will also turn off duplicate bounce notifications and the unhandled delivery notifications. On stage, we have not configured the SNS topics for the email feedback forwarding, no changes there. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great update! I haven't looked at the test file yet, and it looks like you updated it again since I started the review, but here's my first few comments.
emails/views.py
Outdated
if key in source: | ||
return source[key], True | ||
logger.error( | ||
"_get_complaint_data: Unexpected message", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (non-blocking): this seems more like "unexpected message format" - i.e., a message that's missing a key we expect, rather than an unexpected message?
"_get_complaint_data: Unexpected message", | |
"_get_complaint_data: Unexpected message format", |
# Only present for feedback reports | ||
user_agent = complaint.get("userAgent", "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question (non-blocking): does this mean we can use the userAgent
field or value to know whether the end user or their email service provider marked the email as spam?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope. The agent is the mail provider's agent. All complaints from yahoo.com, ymail.com, yahoo.com.mx, yahoo.de, etc. have agent Yahoo!-Mail-Feedback/2.0
. A lot use ReturnPathFBL/2.0
. Microsoft leaves it blank. See the link "Relay Bounces and Complaints v2" on the Relay Engineering page on Confluence.
|
||
T = TypeVar("T") | ||
|
||
def get_or_log( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment (non-blocking): This felt almost generic enough to try to turn into a more general re-usable function. But after a few minutes with ChatGPT, I gave up trying to refactor it.
emails/views.py
Outdated
for email_address, extra_data in complaint_data.complained_recipients: | ||
local, domain = email_address.split("@", 1) | ||
|
||
# For developer mode complaint simulation, swap with developer's email |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit (non-blocking): this code seems specific to AWS complaint simulator, but not necessarily to the "developer mode" controlled by the mask label?
If it is, can we link from here to a doc/comment about the "developer mode"?
# For developer mode complaint simulation, swap with developer's email | |
# For AWS complaint simulation, swap with developer's email |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All comments are "dupes" of 4 main issues:
suggestion (non-blocking): When I woke up this morning I had to remember what the new "developer mode" did. It would be great to copy some of the explanation from that PR into a more permanent doc or comment in the code-base somewhere.
suggestion (non-blocking): Similarly, a lot of the doc-blocks for the new tests assume high-context knowledge, which we may not have next time we have to touch this code. So I made some suggestions for updating the doc-blocks to add more context.
correction (blocking): A couple of the test doc-blocks have copy-pasted incorrect text.
question (blocking): It looks like, unlike the code in Answerprocess_emails_from_sqs
, this complaint-handling code is run in the context of an HTTP callback request from AWS SNS. What does SNS do if it receives a 404 from Relay?
emails/tests/views_tests.py
Outdated
|
||
@override_flag("developer_mode", active=True) | ||
def test_complaint_developer_mode_missing_mask(self): | ||
"""If the simulator email can not be turned into a mask, it is not changed.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sugguestion (non-blocking): add a little more detail here? I don't understand what "simulator email can not be turned into a mask" means.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to move these "cover all the edge cases" tests to a new class, so ComplaintHandlingTest
is more like integration tests.
emails/tests/views_tests.py
Outdated
|
||
@override_flag("developer_mode", active=True) | ||
def test_complaint_developer_mode_domain_address(self): | ||
"""If the simulator email can not be turned into a mask, it is not changed.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same sugguestion (non-blocking): add a little more detail here? I don't understand what "simulator email can not be turned into a mask" means.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm moving these tests of _gather_complainers
out of ComplaintHandlingTest
into a focused test case.
emails/tests/views_tests.py
Outdated
assert rec2.msg == "complaint_notification" | ||
|
||
def test_complaint_no_complained_recipients_error_logged(self): | ||
"""Without complained recipients, an error is logged, but matches on From""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (non-blocking): I had to read thru the code and test fixture to understand what this test was doing.
"""Without complained recipients, an error is logged, but matches on From""" | |
""" | |
Without complainedRecipients, we can still match the user on From:, | |
perform the appropriate action, and log an error | |
""" |
emails/tests/views_tests.py
Outdated
) | ||
|
||
def test_complaint_empty_complained_recipients_error_logged(self): | ||
"""With empty complained recipients, an error is logged, but matches on From""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same suggestion (non-blocking):
"""With empty complained recipients, an error is logged, but matches on From""" | |
""" | |
With complainedRecipients empty, we can still match the user on From:, | |
perform the appropriate action, and log an error | |
""" |
The "source" was the mask, but AWS SES uses the generic Reply-To address. The messageId is set, probably by SES when sending. The null values in commonHeaders are not present. These changes break the mask disable test, which uses the "source". It is marked as an expected failure for now.
If _get_address is called with create=False, pass create=False when calling _get_domain_address. When create=False, _get_domain_address logs less for bad domains, subdomains, etc. It raises DomainAddress.DoesNotExist(). The new _get_address_if_exists calls _get_address(create=False), catches the expected exceptions, and returns None. Add tests for existing and new behavior.
Rewrite _handle_complaint to merge user collection and mask processing. This changes how complaints are handled: * On the first complaint, the auto_block_spam flag is set * On the second complaint, the mask is blocked * If the complaint comes from the AWS SES complaint simulator, swap in the developer's email address for the simulator email address * Log an error if the JSON message is missing mail.commonHeaders.from * Log what action was taken: auto_block_spam, disable_mask, no_action * Log if a mask was found when disabling the mask
Rather than initializing waffle flags each time they are used, initialize once in _sns_message.
This will match '_handle_received: developer_mode'.
1bd8030
to
a0b19f9
Compare
@groovecoder, this should be ready for recheck. Main changes:
Smaller changes:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! 1 small change to the doc file name to make it easier to find for future ENGRs (including us).
@@ -1325,6 +1414,532 @@ def test_build_disabled_mask_for_spam_email(self): | |||
) | |||
|
|||
|
|||
class GetComplaintDataTest(TestCase): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: this is a great test that spells out all the scenarios very clearly. thanks!
assert getattr(err_log, "found_keys") == "complainedRecipients" | ||
|
||
|
||
class GatherComplainersTest(TestCase): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: another clear test case. I was able to quickly scan the docstrings to understand each test.
This replaces #5114, and goes a lot farther.
This rewrites
_handle_complaint
, moving most of the logic into three new helper functions:_get_complaint_data
gets the relevant data out of the complaint notification, with lots of checks and logging if the message is not what we're expecting._gather_complainers
cross-checks the sender and receiver data against the database, getting the related users and masks (RelayAddress
,DomainAddress
). It also checks for the developer "simulate_complaint" mode, and swaps the real user email back in._reduce_future_complaints
takes the actions to reduce future complaints.auto_block_spam
flag to drop emails that AWS SES identifies as spam (same as before)From:
header data, which has the mask, rather than thesource
field, which is our reply address.how?
), it does nothing but takes note that it did nothing (new)This leaves
_handle_complaint
to log the complaint in developer mode, log what happened for each detected Relay user, and emit a counter.I also extended the developer mode to correctly work for domain masks, added
_get_address_if_exists
, removed amypy
override, and fixed Heroku Redis... This PR can definitely be broken into smaller bits.How to test:
The test suite has been expanded to cover the new code. There are lots of new tests.
I pushed this branch to Heroku for manual testing. There are two issues
I've seenthat are now fixed on Heroku:???
and the links do not work. - Fixed the HTML templateHere's the test process:
program:"app/worker.1"
. You'll see logs related to your email.developer_mode
flag to your Heroku user. You can run a Heroku bash shell:heroku run --app fx-private-relay bash
then run
./manage.py waffle_flag developer_mode --append --user "<[email protected]>"
"msg": "_handle_received: developer_mode"
. The"notification_gza85"
fields can be combined and passed toemails.utils.decode_dict_gza85
to get the original delivery notification."msg": "_handle_complaint: developer mode"
. The"notification_gza85"
fields can be combined and passed toemails.utils.decode_dict_gza85
to get the original complaint notification."msg": "complaint_notification"
, and has the standard log for a complaint. The"relay_action"
should be"auto_block_spam"
"relay_action"
asno_action
sinceauto_block_spam
is already on.disable_mask_on_complaint
flag to your Heroku user. You can run a Heroku bash shell:heroku run --app fx-private-relay bash
then run
./manage.py waffle_flag disable_mask_on_complaint --append --user "<[email protected]>"
"relay_action"
is"disable_mask"
(and thenno_action
if there is a second complaint)