def _MakeEmailTasks(self, cnxn, issue, project, config, comment, hostport, users_by_id, pings): """Return a list of dicts for tasks to notify people.""" detail_url = framework_helpers.IssueCommentURL( hostport, project, issue.local_id, seq_num=comment.sequence) fields = sorted((field_def for (field_def, _date_value) in pings), key=lambda fd: fd.field_name) email_data = { 'issue': tracker_views.IssueView(issue, users_by_id, config), 'summary': issue.summary, 'ping_comment_content': comment.content, 'detail_url': detail_url, 'fields': fields, } # Generate two versions of email body: members version has all # full email addresses exposed. body_for_non_members = self.email_template.GetResponse(email_data) framework_views.RevealAllEmails(users_by_id) body_for_members = self.email_template.GetResponse(email_data) logging.info('body for non-members is:\n%r' % body_for_non_members) logging.info('body for members is:\n%r' % body_for_members) contributor_could_view = permissions.CanViewIssue( set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET, project, issue) # Note: We never notify the reporter of any issue just because they # reported it, only if they star it. # TODO(jrobbins): add a user preference for notifying starrers. starrer_ids = [] # TODO(jrobbins): consider IsNoisy() when we support notifying starrers. group_reason_list = notify_reasons.ComputeGroupReasonList( cnxn, self.services, project, issue, config, users_by_id, [], contributor_could_view, starrer_ids=starrer_ids, commenter_in_project=True) commenter_view = users_by_id[comment.user_id] email_tasks = notify_helpers.MakeBulletedEmailWorkItems( group_reason_list, issue, body_for_non_members, body_for_members, project, hostport, commenter_view, detail_url, seq_num=comment.sequence, subject_prefix='Follow up on issue ', compact_subject_prefix='Follow up ') return email_tasks
def testRevealAllEmail(self): users_by_id = { 111: framework_views.StuffUserView(111, '*****@*****.**', True), 222: framework_views.StuffUserView(222, '*****@*****.**', True), 333: framework_views.StuffUserView(333, '*****@*****.**', True), 999: framework_views.StuffUserView(999, '*****@*****.**', True), } # Assert display names are obscured before the reveal. self.assertEqual('*****@*****.**', users_by_id[111].display_name) self.assertEqual('*****@*****.**', users_by_id[222].display_name) self.assertEqual('*****@*****.**', users_by_id[333].display_name) self.assertEqual('*****@*****.**', users_by_id[999].display_name) framework_views.RevealAllEmails(users_by_id) self.assertFalse(users_by_id[111].obscure_email) self.assertFalse(users_by_id[222].obscure_email) self.assertFalse(users_by_id[333].obscure_email) self.assertFalse(users_by_id[999].obscure_email) # Assert display names are now revealed. self.assertEqual('*****@*****.**', users_by_id[111].display_name) self.assertEqual('*****@*****.**', users_by_id[222].display_name) self.assertEqual('*****@*****.**', users_by_id[333].display_name) self.assertEqual('*****@*****.**', users_by_id[999].display_name)
def _MakeEmailTasks( self, cnxn, project, issue, config, old_owner_id, users_by_id, all_comments, comment, starrer_ids, contributor_could_view, hostport, omit_ids, perms): """Formulate emails to be sent.""" detail_url = framework_helpers.IssueCommentURL( hostport, project, issue.local_id, seq_num=comment.sequence) # TODO(jrobbins): avoid the need to make a MonorailRequest object. mr = monorailrequest.MonorailRequest(self.services) mr.project_name = project.project_name mr.project = project mr.perms = perms # We do not autolink in the emails, so just use an empty # registry of autolink rules. # TODO(jrobbins): offer users an HTML email option w/ autolinks. autolinker = autolink.Autolink() was_created = ezt.boolean(comment.sequence == 0) email_data = { # Pass open_related and closed_related into this method and to # the issue view so that we can show it on new issue email. 'issue': tracker_views.IssueView(issue, users_by_id, config), 'summary': issue.summary, 'comment': tracker_views.IssueCommentView( project.project_name, comment, users_by_id, autolinker, {}, mr, issue), 'comment_text': comment.content, 'detail_url': detail_url, 'was_created': was_created, } # Generate three versions of email body: link-only is just the link, # non-members see some obscured email addresses, and members version has # all full email addresses exposed. body_link_only = self.link_only_email_template.GetResponse( {'detail_url': detail_url, 'was_created': was_created}) body_for_non_members = self.email_template.GetResponse(email_data) framework_views.RevealAllEmails(users_by_id) email_data['comment'] = tracker_views.IssueCommentView( project.project_name, comment, users_by_id, autolinker, {}, mr, issue) body_for_members = self.email_template.GetResponse(email_data) logging.info('link-only body is:\n%r' % body_link_only) logging.info('body for non-members is:\n%r' % body_for_non_members) logging.info('body for members is:\n%r' % body_for_members) commenter_email = users_by_id[comment.user_id].email omit_addrs = set([commenter_email] + [users_by_id[omit_id].email for omit_id in omit_ids]) auth = authdata.AuthData.FromUserID( cnxn, comment.user_id, self.services) commenter_in_project = framework_bizobj.UserIsInProject( project, auth.effective_ids) noisy = tracker_helpers.IsNoisy(len(all_comments) - 1, len(starrer_ids)) # Give each user a bullet-list of all the reasons that apply for that user. group_reason_list = notify_reasons.ComputeGroupReasonList( cnxn, self.services, project, issue, config, users_by_id, omit_addrs, contributor_could_view, noisy=noisy, starrer_ids=starrer_ids, old_owner_id=old_owner_id, commenter_in_project=commenter_in_project) commenter_view = users_by_id[comment.user_id] detail_url = framework_helpers.FormatAbsoluteURLForDomain( hostport, issue.project_name, urls.ISSUE_DETAIL, id=issue.local_id) email_tasks = notify_helpers.MakeBulletedEmailWorkItems( group_reason_list, issue, body_link_only, body_for_non_members, body_for_members, project, hostport, commenter_view, detail_url, seq_num=comment.sequence) return email_tasks
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
def _MakeEmailTasks(self, cnxn, issue, project, config, comment, starrer_ids, hostport, users_by_id, pings): """Return a list of dicts for tasks to notify people.""" detail_url = framework_helpers.IssueCommentURL( hostport, project, issue.local_id, seq_num=comment.sequence) fields = sorted((field_def for (field_def, _date_value) in pings), key=lambda fd: fd.field_name) email_data = { 'issue': tracker_views.IssueView(issue, users_by_id, config), 'summary': issue.summary, 'ping_comment_content': comment.content, 'detail_url': detail_url, 'fields': fields, } # Generate three versions of email body with progressively more info. body_link_only = self.link_only_email_template.GetResponse({ 'detail_url': detail_url, 'was_created': ezt.boolean(False) }) body_for_non_members = self.email_template.GetResponse(email_data) framework_views.RevealAllEmails(users_by_id) body_for_members = self.email_template.GetResponse(email_data) logging.info('body for non-members is:\n%r' % body_for_non_members) logging.info('body for members is:\n%r' % body_for_members) contributor_could_view = permissions.CanViewIssue( set(), permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET, project, issue) group_reason_list = notify_reasons.ComputeGroupReasonList( cnxn, self.services, project, issue, config, users_by_id, [], contributor_could_view, starrer_ids=starrer_ids, commenter_in_project=True, include_subscribers=False, include_notify_all=False, starrer_pref_check_function=lambda u: u.notify_starred_ping) commenter_view = users_by_id[comment.user_id] email_tasks = notify_helpers.MakeBulletedEmailWorkItems( group_reason_list, issue, body_link_only, body_for_non_members, body_for_members, project, hostport, commenter_view, detail_url, seq_num=comment.sequence, subject_prefix='Follow up on issue ', compact_subject_prefix='Follow up ') return email_tasks