Exemple #1
0
    def testReplyInvitation(self):
        """We include a footer about replying that is appropriate for that user."""
        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(True, '*****@*****.**', self.member,
                                    REPLY_NOT_ALLOWED), ['reason'], self.issue,
            'body non', 'body mem', self.project, 'example.com',
            self.commenter_view, self.detail_url)
        self.assertEqual(emailfmt.NoReplyAddress(), email_task['reply_to'])
        self.assertNotIn('Reply to this email', email_task['body'])

        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(True, '*****@*****.**', self.member,
                                    REPLY_MAY_COMMENT), ['reason'], self.issue,
            'body non', 'body mem', self.project, 'example.com',
            self.commenter_view, self.detail_url)
        self.assertEqual(
            '%s@%s' % (self.project.project_name, emailfmt.MailDomain()),
            email_task['reply_to'])
        self.assertIn('Reply to this email to add a comment',
                      email_task['body'])
        self.assertNotIn('make changes', email_task['body'])

        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(True, '*****@*****.**', self.member,
                                    REPLY_MAY_UPDATE), ['reason'], self.issue,
            'body non', 'body mem', self.project, 'example.com',
            self.commenter_view, self.detail_url)
        self.assertEqual(
            '%s@%s' % (self.project.project_name, emailfmt.MailDomain()),
            email_task['reply_to'])
        self.assertIn('Reply to this email to add a comment',
                      email_task['body'])
        self.assertIn('make updates', email_task['body'])
  def testComputeGroupReasonList_Subscribers(self):
    """Bob subscribed."""
    sq = tracker_bizobj.MakeSavedQuery(
          1, 'freds issues', 1, 'owner:[email protected]',
          subscription_mode='immediate', executes_in_project_ids=[789])
    self.services.features.UpdateUserSavedQueries(
        'cnxn', self.bob.user_id, [sq])
    actual = notify_reasons.ComputeGroupReasonList(
        'cnxn', self.services, self.project, self.issue, self.config,
        self.users_by_id, [], True)
    self.CheckGroupReasonList(
        actual,
        owner_apl=[notify_reasons.AddrPerm(
            False, self.fred.email, self.fred, REPLY_NOT_ALLOWED)],
        subscriber_apl=[notify_reasons.AddrPerm(
            False, self.bob.email, self.bob, REPLY_NOT_ALLOWED)])

    # Now with subscriber notifications disabled.
    actual = notify_reasons.ComputeGroupReasonList(
        'cnxn', self.services, self.project, self.issue, self.config,
        self.users_by_id, [], True, include_subscribers=False)
    self.CheckGroupReasonList(
        actual,
        owner_apl=[notify_reasons.AddrPerm(
            False, self.fred.email, self.fred, REPLY_NOT_ALLOWED)])
  def testComputeGroupReasonList_NotifyAll(self):
    """Project is configured to always notify [email protected]."""
    self.project.issue_notify_address = '*****@*****.**'
    actual = notify_reasons.ComputeGroupReasonList(
        'cnxn', self.services, self.project, self.issue, self.config,
        self.users_by_id, [], True)
    self.CheckGroupReasonList(
        actual,
        owner_apl=[notify_reasons.AddrPerm(
            False, self.fred.email, self.fred, REPLY_NOT_ALLOWED)],
        all_notifications_apl=[notify_reasons.AddrPerm(
            False, '*****@*****.**', None, REPLY_NOT_ALLOWED)])

    # We don't use the notify-all address when the issue is not public.
    actual = notify_reasons.ComputeGroupReasonList(
        'cnxn', self.services, self.project, self.issue, self.config,
        self.users_by_id, [], False)
    self.CheckGroupReasonList(
        actual,
        owner_apl=[notify_reasons.AddrPerm(
            False, self.fred.email, self.fred, REPLY_NOT_ALLOWED)])

    # Now with the notify-all address disabled.
    actual = notify_reasons.ComputeGroupReasonList(
        'cnxn', self.services, self.project, self.issue, self.config,
        self.users_by_id, [], True, include_notify_all=False)
    self.CheckGroupReasonList(
        actual,
        owner_apl=[notify_reasons.AddrPerm(
            False, self.fred.email, self.fred, REPLY_NOT_ALLOWED)])
 def testComputeGroupReasonList_OwnerAndCC(self):
   """Fred owns the issue, Alice is CC'd."""
   self.issue.cc_ids = [self.alice.user_id]
   actual = notify_reasons.ComputeGroupReasonList(
       'cnxn', self.services, self.project, self.issue, self.config,
       self.users_by_id, [], True)
   self.CheckGroupReasonList(
       actual,
       owner_apl=[notify_reasons.AddrPerm(
           False, self.fred.email, self.fred, REPLY_NOT_ALLOWED)],
       ccd_apl=[notify_reasons.AddrPerm(
           False, self.alice.email, self.alice, REPLY_NOT_ALLOWED)])
 def testComputeGroupReasonList_Starrers(self):
   """Bob and Alice starred it, but Alice opts out of notifications."""
   self.alice.notify_starred_issue_change = False
   actual = notify_reasons.ComputeGroupReasonList(
       'cnxn', self.services, self.project, self.issue, self.config,
       self.users_by_id, [], True,
       starrer_ids=[self.alice.user_id, self.bob.user_id])
   self.CheckGroupReasonList(
       actual,
       owner_apl=[notify_reasons.AddrPerm(
           False, self.fred.email, self.fred, REPLY_NOT_ALLOWED)],
       starrer_apl=[notify_reasons.AddrPerm(
           False, self.bob.email, self.bob, REPLY_NOT_ALLOWED)])
 def testRecipientIsMember(self):
   cnxn = 'fake cnxn'
   ids_to_consider = [111, 222, 999]
   addr_perm_list = notify_reasons.ComputeIssueChangeAddressPermList(
       cnxn, ids_to_consider, self.project, self.issue, self.services, set(),
       self.users_by_id, pref_check_function=lambda *args: True)
   self.assertEqual(
       [notify_reasons.AddrPerm(
           True, '*****@*****.**', self.owner, REPLY_MAY_UPDATE),
        notify_reasons.AddrPerm(
           True, '*****@*****.**', self.member, REPLY_MAY_UPDATE),
        notify_reasons.AddrPerm(
           False, '*****@*****.**', self.visitor, REPLY_MAY_COMMENT)],
       addr_perm_list)
Exemple #7
0
    def testFormatBulkIssues_LinkOnly_Multiple(self):
        """A user may not see full notification details for some changed issue."""
        self.issue1.summary = 'one summary'
        self.issue1.labels = ['Restrict-View-Google']
        self.issue2.summary = 'two summary'
        task = notify.NotifyBulkChangeTask(request=None,
                                           response=None,
                                           services=self.services)
        users_by_id = {}
        commenter_view = None
        config = self.services.config.GetProjectConfig('cnxn', 12345)
        addrperm = notify_reasons.AddrPerm(False, '*****@*****.**',
                                           self.nonmember,
                                           notify_reasons.REPLY_NOT_ALLOWED,
                                           None)

        subject, body = task._FormatBulkIssues([self.issue1, self.issue2],
                                               users_by_id, commenter_view,
                                               'localhost:8080',
                                               'test comment', [], config,
                                               addrperm)

        self.assertIn('2 issues', subject)
        self.assertNotIn('summary', subject)
        self.assertNotIn('one summary', body)
        self.assertIn('two summary', body)
        self.assertNotIn('test comment', body)
    def testHtmlBody_WithEscapedHtml(self):
        """"An html body is sent with html content escaped."""
        body_with_html_content = (
            '<a href="http://www.google.com">test</a> \'something\'')
        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(False, '*****@*****.**',
                                    self.member, REPLY_NOT_ALLOWED,
                                    user_pb2.UserPrefs()), ['reason'],
            self.issue, 'body link-only', body_with_html_content,
            'unused body mem', self.project, 'example.com',
            self.commenter_view, self.detail_url)

        escaped_body_with_html_content = (
            '&lt;a href=&quot;http://www.google.com&quot;&gt;test&lt;/a&gt; '
            '&#39;something&#39;')
        notify_helpers._MakeNotificationFooter(['reason'], REPLY_NOT_ALLOWED,
                                               'example.com')
        expected_html_body = (
            notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
                'url':
                self.detail_url,
                'body':
                '%s-- <br/>%s' %
                (escaped_body_with_html_content, self.expected_html_footer)
            })
        self.assertEquals(expected_html_body, email_task['html_body'])
    def testNotifyAddress(self):
        # No mailing list or filter rules are defined
        addr_perm_list = notify_reasons.ComputeProjectNotificationAddrList(
            self.cnxn, self.services, self.project, True, set())
        self.assertListEqual([], addr_perm_list)

        # Only mailing list is notified.
        self.project.issue_notify_address = '*****@*****.**'
        addr_perm_list = notify_reasons.ComputeProjectNotificationAddrList(
            self.cnxn, self.services, self.project, True, set())
        self.assertListEqual([
            notify_reasons.AddrPerm(False, '*****@*****.**', None,
                                    REPLY_NOT_ALLOWED, user_pb2.UserPrefs())
        ], addr_perm_list)

        # No one is notified because mailing list was already notified.
        omit_addrs = {'*****@*****.**'}
        addr_perm_list = notify_reasons.ComputeProjectNotificationAddrList(
            self.cnxn, self.services, self.project, False, omit_addrs)
        self.assertListEqual([], addr_perm_list)

        # No one is notified because anon users cannot view.
        addr_perm_list = notify_reasons.ComputeProjectNotificationAddrList(
            self.cnxn, self.services, self.project, False, set())
        self.assertListEqual([], addr_perm_list)
 def testBodySelection_Member(self):
     """We send members the email body that is indented for members."""
     email_task = notify_helpers._MakeEmailWorkItem(
         notify_reasons.AddrPerm(True, '*****@*****.**',
                                 self.member, REPLY_NOT_ALLOWED,
                                 user_pb2.UserPrefs()), ['reason'],
         self.issue, 'body link-only', 'body mem', 'body mem', self.project,
         'example.com', self.commenter_view, self.detail_url)
     self.assertIn('body mem', email_task['body'])
Exemple #11
0
 def testInboundEmailDisabled(self):
     """We don't invite replies if they are disabled for this project."""
     self.project.process_inbound_email = False
     email_task = notify_helpers._MakeEmailWorkItem(
         notify_reasons.AddrPerm(True, '*****@*****.**', self.member,
                                 REPLY_MAY_UPDATE), ['reason'], self.issue,
         'body non', 'body mem', self.project, 'example.com',
         self.commenter_view, self.detail_url)
     self.assertEqual(emailfmt.NoReplyAddress(), email_task['reply_to'])
Exemple #12
0
    def testReasons(self):
        """The footer lists reasons why that email was sent to that user."""
        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(True, '*****@*****.**', self.member,
                                    REPLY_MAY_UPDATE),
            ['Funny', 'Caring', 'Near'], self.issue, 'body', 'body',
            self.project, 'example.com', self.commenter_view, self.detail_url)
        self.assertIn('because:', email_task['body'])
        self.assertIn('1. Funny', email_task['body'])
        self.assertIn('2. Caring', email_task['body'])
        self.assertIn('3. Near', email_task['body'])

        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(True, '*****@*****.**', self.member,
                                    REPLY_MAY_UPDATE), [], self.issue, 'body',
            'body', self.project, 'example.com', self.commenter_view,
            self.detail_url)
        self.assertNotIn('because', email_task['body'])
 def testBodySelection_LinkOnly(self, mock_sulo):
     """We send a link-only body when ShouldUseLinkOnly() is true."""
     mock_sulo.return_value = True
     email_task = notify_helpers._MakeEmailWorkItem(
         notify_reasons.AddrPerm(True, '*****@*****.**',
                                 self.member, REPLY_NOT_ALLOWED,
                                 user_pb2.UserPrefs()), ['reason'],
         self.issue, 'body link-only', 'body mem', 'body mem', self.project,
         'example.com', self.commenter_view, self.detail_url)
     self.assertIn('body link-only', email_task['body'])
Exemple #14
0
 def setUp(self):
     parent = user_pb2.User(user_id=111,
                            email='*****@*****.**',
                            linked_child_ids=[222])
     child = user_pb2.User(user_id=222,
                           email='*****@*****.**',
                           linked_parent_id=111)
     user_3 = user_pb2.User(user_id=333, email='*****@*****.**')
     user_4 = user_pb2.User(user_id=444, email='*****@*****.**')
     self.addr_perm_parent = notify_reasons.AddrPerm(
         False, parent.email, parent, notify_reasons.REPLY_NOT_ALLOWED)
     self.addr_perm_child = notify_reasons.AddrPerm(
         False, child.email, child, notify_reasons.REPLY_NOT_ALLOWED)
     self.addr_perm_3 = notify_reasons.AddrPerm(
         False, user_3.email, user_3, notify_reasons.REPLY_NOT_ALLOWED)
     self.addr_perm_4 = notify_reasons.AddrPerm(
         False, user_4.email, user_4, notify_reasons.REPLY_NOT_ALLOWED)
     self.addr_perm_5 = notify_reasons.AddrPerm(
         False, '*****@*****.**', None, notify_reasons.REPLY_NOT_ALLOWED)
Exemple #15
0
    def testHtmlBody(self):
        """"An html body is sent if a detail_url is specified."""
        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(False, '*****@*****.**', self.member,
                                    REPLY_NOT_ALLOWED), ['reason'], self.issue,
            'body non', 'body mem', self.project, 'example.com',
            self.commenter_view, self.detail_url)

        expected_html_body = (
            notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
                'url': self.detail_url,
                'body': 'body non-- <br/>%s' % self.expected_html_footer
            })
        self.assertEquals(expected_html_body, email_task['html_body'])
Exemple #16
0
    def testBodySelection(self):
        """We send non-members the email body that is indented for non-members."""
        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(False, '*****@*****.**', self.member,
                                    REPLY_NOT_ALLOWED), ['reason'], self.issue,
            'body non', 'body mem', self.project, 'example.com',
            self.commenter_view, self.detail_url)

        self.assertEqual('*****@*****.**', email_task['to'])
        self.assertEqual('Issue 1234 in proj1: summary', email_task['subject'])
        self.assertIn('body non', email_task['body'])
        self.assertEqual(
            emailfmt.FormatFromAddr(self.project,
                                    commenter_view=self.commenter_view,
                                    can_reply_to=False),
            email_task['from_addr'])
        self.assertEqual(emailfmt.NoReplyAddress(), email_task['reply_to'])

        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(True, '*****@*****.**', self.member,
                                    REPLY_NOT_ALLOWED), ['reason'], self.issue,
            'body mem', 'body mem', self.project, 'example.com',
            self.commenter_view, self.detail_url)
        self.assertIn('body mem', email_task['body'])
Exemple #17
0
    def testHtmlBody_LinkWithinTags(self):
        """"An html body is sent with correct <a href>s."""
        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(False, '*****@*****.**', self.member,
                                    REPLY_NOT_ALLOWED), ['reason'], self.issue,
            'a <http://google.com> z', 'unused body', self.project,
            'example.com', self.commenter_view, self.detail_url)

        expected_html_body = (
            notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
                'url':
                self.detail_url,
                'body':
                ('a &lt;<a href="http://google.com">http://google.com</a>&gt; '
                 'z-- <br/>%s' % self.expected_html_footer)
            })
        self.assertEquals(expected_html_body, email_task['html_body'])
    def testFilterRuleNotifyAddresses(self):
        issue = fake.MakeTestIssue(self.project.project_id, 1, 'summary',
                                   'New', 555)
        issue.derived_notify_addrs.extend(['*****@*****.**'])

        addr_perm_list = notify_reasons.ComputeIssueNotificationAddrList(
            self.cnxn, self.services, issue, set())
        self.assertListEqual([
            notify_reasons.AddrPerm(False, '*****@*****.**', None,
                                    REPLY_NOT_ALLOWED, user_pb2.UserPrefs())
        ], addr_perm_list)

        # Also-notify addresses can be omitted (e.g., if it is the same as
        # the email address of the user who made the change).
        addr_perm_list = notify_reasons.ComputeIssueNotificationAddrList(
            self.cnxn, self.services, issue, {'*****@*****.**'})
        self.assertListEqual([], addr_perm_list)
Exemple #19
0
    def testHtmlBody_WithLinks(self):
        """"An html body is sent if a detail_url is specified."""
        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(False, '*****@*****.**', self.member,
                                    REPLY_NOT_ALLOWED), ['reason'], self.issue,
            'test google.com test', 'unused body mem', self.project,
            'example.com', self.commenter_view, self.detail_url)

        expected_html_body = (
            notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
                'url':
                self.detail_url,
                'body':
                ('test <a href="http://google.com">google.com</a> test-- <br/>%s'
                 % (self.expected_html_footer))
            })
        self.assertEquals(expected_html_body, email_task['html_body'])
Exemple #20
0
    def testHtmlBody_WithUnicodeChars(self):
        """"An html body is sent if a detail_url is specified."""
        unicode_content = '\xe2\x9d\xa4     â    â'
        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(False, '*****@*****.**', self.member,
                                    REPLY_NOT_ALLOWED), ['reason'], self.issue,
            unicode_content, 'unused body mem', self.project, 'example.com',
            self.commenter_view, self.detail_url)

        expected_html_body = (
            notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
                'url':
                self.detail_url,
                'body':
                '%s-- <br/>%s' %
                (unicode_content.decode('utf-8'), self.expected_html_footer)
            })
        self.assertEquals(expected_html_body, email_task['html_body'])
    def testHtmlBody_EmailWithinTags(self):
        """"An html body is sent with correct <a href>s."""
        email_task = notify_helpers._MakeEmailWorkItem(
            notify_reasons.AddrPerm(False, '*****@*****.**',
                                    self.member, REPLY_NOT_ALLOWED,
                                    user_pb2.UserPrefs()), ['reason'],
            self.issue, 'body link-only',
            'a <*****@*****.**> <*****@*****.**> z', 'unused body mem',
            self.project, 'example.com', self.commenter_view, self.detail_url)

        expected_html_body = (
            notify_helpers.HTML_BODY_WITH_GMAIL_ACTION_TEMPLATE % {
                'url':
                self.detail_url,
                'body':
                ('a &lt;<a href="mailto:[email protected]">[email protected]</a>&gt;'
                 ' &lt;<a href="mailto:[email protected]">[email protected]</a>&gt; '
                 'z-- <br/>%s' % self.expected_html_footer)
            })
        self.assertEquals(expected_html_body, email_task['html_body'])
 def setUp(self):
     self.user_prefs = user_pb2.UserPrefs()
     self.user = user_pb2.User()
     self.issue = fake.MakeTestIssue(789, 1, 'summary one', 'New', 111)
     self.rvg_issue = fake.MakeTestIssue(789,
                                         2,
                                         'summary two',
                                         'New',
                                         111,
                                         labels=['Restrict-View-Google'])
     self.more_restricted_issue = fake.MakeTestIssue(
         789, 3, 'summary three', 'New', 111, labels=['Restrict-View-Core'])
     self.both_restricted_issue = fake.MakeTestIssue(
         789,
         4,
         'summary four',
         'New',
         111,
         labels=['Restrict-View-Google', 'Restrict-View-Core'])
     self.addr_perm = notify_reasons.AddrPerm(
         False, '*****@*****.**', self.user,
         notify_reasons.REPLY_MAY_COMMENT, self.user_prefs)
Exemple #23
0
    def testFormatBulkIssues_Normal_Single(self):
        """A user may see full notification details for all changed issues."""
        self.issue1.summary = 'one summary'
        task = notify.NotifyBulkChangeTask(request=None,
                                           response=None,
                                           services=self.services)
        users_by_id = {}
        commenter_view = None
        config = self.services.config.GetProjectConfig('cnxn', 12345)
        addrperm = notify_reasons.AddrPerm(False, '*****@*****.**',
                                           self.nonmember,
                                           notify_reasons.REPLY_NOT_ALLOWED,
                                           None)

        subject, body = task._FormatBulkIssues([self.issue1], users_by_id,
                                               commenter_view,
                                               'localhost:8080',
                                               'test comment', [], config,
                                               addrperm)

        self.assertIn('one summary', subject)
        self.assertIn('one summary', body)
        self.assertIn('test comment', body)
Exemple #24
0
  def _BulkEditEmailTasks(
      self, cnxn, issues, old_owner_ids, omit_addrs, project,
      non_private_issues, users_by_id, ids_in_issues, starrers,
      commenter_view, hostport, comment_text, amendments, config):
    """Generate Email PBs to notify interested users after a bulk edit."""
    # 1. Get the user IDs of everyone who could be notified,
    # and make all their user proxies. Also, build a dictionary
    # of all the users to notify and the issues that they are
    # interested in.  Also, build a dictionary of additional email
    # addresses to notify and the issues to notify them of.
    users_by_id = {}
    ids_to_notify_of_issue = {}
    additional_addrs_to_notify_of_issue = collections.defaultdict(list)

    users_to_queries = notify_reasons.GetNonOmittedSubscriptions(
        cnxn, self.services, [project.project_id], {})
    config = self.services.config.GetProjectConfig(
        cnxn, project.project_id)
    for issue, old_owner_id in zip(issues, old_owner_ids):
      issue_participants = set(
          [tracker_bizobj.GetOwnerId(issue), old_owner_id] +
          tracker_bizobj.GetCcIds(issue))
      # users named in user-value fields that notify.
      for fd in config.field_defs:
        issue_participants.update(
            notify_reasons.ComputeNamedUserIDsToNotify(issue.field_values, fd))
      for user_id in ids_in_issues[issue.local_id]:
        # TODO(jrobbins): implement batch GetUser() for speed.
        if not user_id:
          continue
        auth = authdata.AuthData.FromUserID(
            cnxn, user_id, self.services)
        if (auth.user_pb.notify_issue_change and
            not auth.effective_ids.isdisjoint(issue_participants)):
          ids_to_notify_of_issue.setdefault(user_id, []).append(issue)
        elif (auth.user_pb.notify_starred_issue_change and
              user_id in starrers[issue.local_id]):
          # Skip users who have starred issues that they can no longer view.
          starrer_perms = permissions.GetPermissions(
              auth.user_pb, auth.effective_ids, project)
          granted_perms = tracker_bizobj.GetGrantedPerms(
              issue, auth.effective_ids, config)
          starrer_can_view = permissions.CanViewIssue(
              auth.effective_ids, starrer_perms, project, issue,
              granted_perms=granted_perms)
          if starrer_can_view:
            ids_to_notify_of_issue.setdefault(user_id, []).append(issue)
        logging.info(
            'ids_to_notify_of_issue[%s] = %s',
            user_id,
            [i.local_id for i in ids_to_notify_of_issue.get(user_id, [])])

      # Find all subscribers that should be notified.
      subscribers_to_consider = notify_reasons.EvaluateSubscriptions(
          cnxn, issue, users_to_queries, self.services, config)
      for sub_id in subscribers_to_consider:
        auth = authdata.AuthData.FromUserID(cnxn, sub_id, self.services)
        sub_perms = permissions.GetPermissions(
            auth.user_pb, auth.effective_ids, project)
        granted_perms = tracker_bizobj.GetGrantedPerms(
            issue, auth.effective_ids, config)
        sub_can_view = permissions.CanViewIssue(
            auth.effective_ids, sub_perms, project, issue,
            granted_perms=granted_perms)
        if sub_can_view:
          ids_to_notify_of_issue.setdefault(sub_id, [])
          if issue not in ids_to_notify_of_issue[sub_id]:
            ids_to_notify_of_issue[sub_id].append(issue)

      if issue in non_private_issues:
        for notify_addr in issue.derived_notify_addrs:
          additional_addrs_to_notify_of_issue[notify_addr].append(issue)

    # 2. Compose an email specifically for each user, and one email to each
    # notify_addr with all the issues that it.
    # Start from non-members first, then members to reveal email addresses.
    email_tasks = []
    needed_user_view_ids = [uid for uid in ids_to_notify_of_issue
                            if uid not in users_by_id]
    users_by_id.update(framework_views.MakeAllUserViews(
        cnxn, self.services.user, needed_user_view_ids))
    member_ids_to_notify_of_issue = {}
    non_member_ids_to_notify_of_issue = {}
    member_additional_addrs = {}
    non_member_additional_addrs = {}
    addr_to_addrperm = {}  # {email_address: AddrPerm object}
    all_user_prefs = self.services.user.GetUsersPrefs(
        cnxn, ids_to_notify_of_issue)

    # TODO(jrobbins): Merge ids_to_notify_of_issue entries for linked accounts.

    for user_id in ids_to_notify_of_issue:
      if not user_id:
        continue  # Don't try to notify NO_USER_SPECIFIED
      if users_by_id[user_id].email in omit_addrs:
        logging.info('Omitting %s', user_id)
        continue
      user_issues = ids_to_notify_of_issue[user_id]
      if not user_issues:
        continue  # user's prefs indicate they don't want these notifications
      auth = authdata.AuthData.FromUserID(
          cnxn, user_id, self.services)
      is_member = bool(framework_bizobj.UserIsInProject(
          project, auth.effective_ids))
      if is_member:
        member_ids_to_notify_of_issue[user_id] = user_issues
      else:
        non_member_ids_to_notify_of_issue[user_id] = user_issues
      addr = users_by_id[user_id].email
      omit_addrs.add(addr)
      addr_to_addrperm[addr] = notify_reasons.AddrPerm(
          is_member, addr, users_by_id[user_id].user,
          notify_reasons.REPLY_NOT_ALLOWED, all_user_prefs[user_id])

    for addr, addr_issues in additional_addrs_to_notify_of_issue.items():
      auth = None
      try:
        auth = authdata.AuthData.FromEmail(cnxn, addr, self.services)
      except:  # pylint: disable=bare-except
        logging.warning('Cannot find user of email %s ', addr)
      if auth:
        is_member = bool(framework_bizobj.UserIsInProject(
            project, auth.effective_ids))
      else:
        is_member = False
      if is_member:
        member_additional_addrs[addr] = addr_issues
      else:
        non_member_additional_addrs[addr] = addr_issues
      omit_addrs.add(addr)
      addr_to_addrperm[addr] = notify_reasons.AddrPerm(
          is_member, addr, None, notify_reasons.REPLY_NOT_ALLOWED, None)

    for user_id, user_issues in non_member_ids_to_notify_of_issue.items():
      addr = users_by_id[user_id].email
      email = self._FormatBulkIssuesEmail(
          addr_to_addrperm[addr], user_issues, users_by_id,
          commenter_view, hostport, comment_text, amendments, config, project)
      email_tasks.append(email)
      logging.info('about to bulk notify non-member %s (%s) of %s',
                   users_by_id[user_id].email, user_id,
                   [issue.local_id for issue in user_issues])

    for addr, addr_issues in non_member_additional_addrs.items():
      email = self._FormatBulkIssuesEmail(
          addr_to_addrperm[addr], addr_issues, users_by_id, commenter_view,
          hostport, comment_text, amendments, config, project)
      email_tasks.append(email)
      logging.info('about to bulk notify non-member additional addr %s of %s',
                   addr, [addr_issue.local_id for addr_issue in addr_issues])

    framework_views.RevealAllEmails(users_by_id)
    commenter_view.RevealEmail()

    for user_id, user_issues in member_ids_to_notify_of_issue.items():
      addr = users_by_id[user_id].email
      email = self._FormatBulkIssuesEmail(
          addr_to_addrperm[addr], user_issues, users_by_id,
          commenter_view, hostport, comment_text, amendments, config, project)
      email_tasks.append(email)
      logging.info('about to bulk notify member %s (%s) of %s',
                   addr, user_id, [issue.local_id for issue in user_issues])

    for addr, addr_issues in member_additional_addrs.items():
      email = self._FormatBulkIssuesEmail(
          addr_to_addrperm[addr], addr_issues, users_by_id, commenter_view,
          hostport, comment_text, amendments, config, project)
      email_tasks.append(email)
      logging.info('about to bulk notify member additional addr %s of %s',
                   addr, [addr_issue.local_id for addr_issue in addr_issues])

    # 4. Add in the project's issue_notify_address.  This happens even if it
    # is the same as the commenter's email address (which would be an unusual
    # but valid project configuration).  Only issues that any contributor could
    # view are included in emails to the all-issue-activity mailing lists.
    if (project.issue_notify_address
        and project.issue_notify_address not in omit_addrs):
      non_private_issues_live = []
      for issue in issues:
        contributor_could_view = permissions.CanViewIssue(
            set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET,
            project, issue)
        if contributor_could_view:
          non_private_issues_live.append(issue)

      if non_private_issues_live:
        project_notify_addrperm = notify_reasons.AddrPerm(
            True, project.issue_notify_address, None,
            notify_reasons.REPLY_NOT_ALLOWED, None)
        email = self._FormatBulkIssuesEmail(
            project_notify_addrperm, non_private_issues_live,
            users_by_id, commenter_view, hostport, comment_text, amendments,
            config, project)
        email_tasks.append(email)
        omit_addrs.add(project.issue_notify_address)
        logging.info('about to bulk notify all-issues %s of %s',
                     project.issue_notify_address,
                     [issue.local_id for issue in non_private_issues])

    return email_tasks