• 
      

    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.