Cleanup and refactor the e-mail module

Review Request #7560 — Created July 30, 2015 and submitted

Information

Review Board
release-2.0.x

Reviewers

The recipient building portion of send_review_mail has been
refactored out into two methods: build_recipients, which returns sets
of Users and Groups that should receive the e-mail, as well as
recipients_to_addresses, which builds sets of addresses with fewer
queries than before.

The logic of determing the recipients of an e-mail has been moved out
of send_review_mail and the signature of the function has changed to
accept the TO and CC fields as sets or lists of Users and
Groups. The logic has also been corrected to no longer send e-mails
to the following:

  • users who have starred a review request but are no longer in the
    review request's local site;
  • users in the limit_recipients_to list who are inactive; and
  • users in the limit_recipients_to list who are no longer part of
    the review request's local site.

All methods that call into send_review_mail to send e-mail now must
calculate the addresses they will be sending to. This is to allow for
a future change to allow extensions to filter the TO and CC
fields of e-mails.

A large portion of this change is a style and docstring cleanup to
bring it in line with our style guidelines as well as adding numerous
new unit tests to cover all the behaviour of the two new functions
and ensure it does not regress.

Ran unit tests.

Description From Last Updated

"through the", here and below.

chipx86chipx86

I'd say "site configuration setting".

chipx86chipx86

Missing "full name".

chipx86chipx86

Missing period.

chipx86chipx86

I think to link this right, it'll need to be reviewboard.reviews.models.groups.Group. You'd have to check with a docs build.

chipx86chipx86

This is pretty old code. We can just do: if ',' not in group.mailing_list:

chipx86chipx86

Blank line between these.

chipx86chipx86

Col: 1 E302 expected 2 blank lines, found 1

reviewbotreviewbot

Missing a trailing period.

chipx86chipx86

Before, extra_recipients was a list of usernames. Now it's treated as a list of users. Is that correct? If so, …

chipx86chipx86

This is checking if the function is present. It needs to be called.

chipx86chipx86

I know I provided the code snippet earlier, but I think if we do is_active=True first, it'll let the DB …

chipx86chipx86

Same here.

chipx86chipx86

This will fail if the , is first, but anyway, it should be: if ',' in recipient.mailing_list:

chipx86chipx86

We should specifically check if it's a string, and have an else that logs an error saying it's an unknown …

chipx86chipx86

Should be one line.

chipx86chipx86

Missing docs.

chipx86chipx86

Alignment problem. Also, in this case, you can pass a list or tuple in to issubset(). No need to build …

chipx86chipx86

'QuerySet' imported but unused

reviewbotreviewbot

While we're busy making things more correct, we should replace this with a call to email.utils.formataddr(), which will do correct …

daviddavid

formataddr will do the right thing if the full name is falsy.

daviddavid

This doesn't really work because these sets can now contain Group objects that users can be members of but we …

EL elatt

Why is the cc_field set not being discarded as well? Also, it is unfortunate that we have to duplicate this …

EL elatt
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
  2. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues
    Col: 1
     E302 expected 2 blank lines, found 1
    
  3. 
      
chipx86
  1. Wow, big change. Looks like some great performance improvements.

    Not that I want to add to your plate, but I'm going to want to see more unit tests covering more combinations of things. All the different conditionals we now have for choosing what ends up in sets should be tested.

    Since it's broken up now, these should be easier to test. The new utility functions can be individually tested with every combination, and the send e-mail functions can be tested with the knowledge that those utility functions are working.

    E-mail is pretty critical, and any bug will affect nearly every install, so it's just absolutely crucial that nothing slips by us here.

  2. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    "through the", here and below.

  3. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    I'd say "site configuration setting".

  4. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    Missing "full name".

  5. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    Missing period.

  6. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    I think to link this right, it'll need to be reviewboard.reviews.models.groups.Group.

    You'd have to check with a docs build.

  7. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    This is pretty old code. We can just do:

    if ',' not in group.mailing_list:
    
  8. reviewboard/notifications/email.py (Diff revision 1)
     
     
     
    Show all issues

    Blank line between these.

  9. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    Missing a trailing period.

  10. reviewboard/notifications/email.py (Diff revision 1)
     
     
     
     
     
    Show all issues

    Before, extra_recipients was a list of usernames. Now it's treated as a list of users. Is that correct?

    If so, we should make sure to specify exactly what's expected in the docs above. We should also sanity-check the contents and log a suitable error about migrating the call, in case an extension is already calling this manually (I think there are some out there that do).

  11. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    This is checking if the function is present. It needs to be called.

  12. reviewboard/notifications/email.py (Diff revision 1)
     
     
     
     
    Show all issues

    I know I provided the code snippet earlier, but I think if we do is_active=True first, it'll let the DB engine check that before checking the joins.

  13. reviewboard/notifications/email.py (Diff revision 1)
     
     
     
    Show all issues

    Same here.

  14. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    This will fail if the , is first, but anyway, it should be:

    if ',' in recipient.mailing_list:
    
  15. reviewboard/notifications/email.py (Diff revision 1)
     
     
     
    Show all issues

    We should specifically check if it's a string, and have an else that logs an error saying it's an unknown value, showing the repr of the value.

    1. This should only ever be a User or a Group becuase its called from within the email module itself.

  16. reviewboard/notifications/email.py (Diff revision 1)
     
     
     
    Show all issues

    Should be one line.

  17. reviewboard/notifications/email.py (Diff revision 1)
     
     
    Show all issues

    Missing docs.

  18. reviewboard/notifications/email.py (Diff revision 1)
     
     
     
    Show all issues

    Alignment problem.

    Also, in this case, you can pass a list or tuple in to issubset(). No need to build a set of the data.

  19. 
      
brennie
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
  2. reviewboard/notifications/email.py (Diff revision 2)
     
     
    Show all issues
     'QuerySet' imported but unused
    
  3. 
      
brennie
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
  2. 
      
brennie
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
  2. 
      
david
  1. 
      
  2. reviewboard/notifications/email.py (Diff revision 4)
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
    Show all issues

    While we're busy making things more correct, we should replace this with a call to email.utils.formataddr(), which will do correct RFC 2047 encoding.

  3. 
      
brennie
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
  2. 
      
david
  1. 
      
  2. reviewboard/notifications/email.py (Diff revisions 4 - 5)
     
     
     
     
     
    Show all issues

    formataddr will do the right thing if the full name is falsy.

  3. 
      
brennie
reviewbot
  1. Tool: Pyflakes
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
    
    Tool: PEP8 Style Checker
    Processed Files:
        reviewboard/notifications/email.py
        reviewboard/notifications/tests.py
    
    
  2. 
      
david
  1. Ship It!
  2. 
      
brennie
Review request changed
Status:
Completed
Change Summary:
Pushed to release-2.0.x (0734e87)
EL
  1. The current code doesn't quite work for users that have should_send_own_updates set to false, and a review is assigned to a group that the user is a member of, and this change is by said user. The problem is that the earlier filtering in build_recipients() can't detect this case b/c it just sees opaque Group objects, then in send_review_mail() these lists of User and Group objects get converted to lists of email addrs and we go and process should_send_own_updates again but for some reason ignore the cc_field list which is where the user's email addr is a member of now.
    
    Who are the clients of build_recipients()? Is it possible for build_recipients() to expand the User and Group objects into email addrs before returning its two sets? If we could, we could completely remove all that extra logic from send_review_mail() and it can just worry about sending email to the exact list of email addrs it was given. Otherwise, the only solution I see is that the should_send_own_updates() logic in build_recipients() get removed and the contract for the method should be updated to explicitly state that it is up to the clients of the method to do that processing (for both the to and cc field sets).
    1. The reason that build_recipients operates the way it does is for e-mail extensions, which allow adding/removing users and groups to e-mails before they are sent. See this review request.

      This isn't really the place to file issues with the code now that is landed. Can we keep the discussion to the bug report?

  2. reviewboard/notifications/email.py (Diff revision 6)
     
     
     
     
    Show all issues
    This doesn't really work because these sets can now contain Group objects that users can be members of but we don't expand the groups until outside of the method.
    1. And in send_review_mail we will again filter out the user's email...

  3. reviewboard/notifications/email.py (Diff revision 6)
     
     
     
    Show all issues
    Why is the cc_field set not being discarded as well? Also, it is unfortunate that we have to duplicate this logic here. It would be easier to test if this logic was outside of this method
    1. On line 490:

      cc_field = recipeints_to_addresses(cc_field) - to_field
      

      This ensures that any address in to_field is not in cc_field, so the user's e-mail address cannot be in cc_field.

  4.