def get_unified_diff(old_text, new_text, text_width): r"""Return a unified diff of the two texts. Before the diff is produced, the texts are wrapped to the given text width. >>> print get_unified_diff( ... 'Some text\nAnother line\n',get_un ... 'Some more text\nAnother line\n', ... text_width=72) - Some text + Some more text Another line """ mailwrapper = MailWrapper(width=72) old_text_wrapped = mailwrapper.format(old_text or "") new_text_wrapped = mailwrapper.format(new_text or "") lines_of_context = len(old_text_wrapped.splitlines()) text_diff = unified_diff(old_text_wrapped.splitlines(), new_text_wrapped.splitlines(), n=lines_of_context) # Remove the diff header, which consists of the first three # lines. text_diff = list(text_diff)[3:] # Let's simplify the diff output by removing the helper lines, # which begin with '?'. text_diff = [diff_line for diff_line in text_diff if not diff_line.startswith("?")] # Add a whitespace between the +/- and the text line. text_diff = [re.sub("^([\+\- ])(.*)", r"\1 \2", line) for line in text_diff] text_diff = "\n".join(text_diff) return text_diff
def buildBody(self, rationale): """See `IQuestionEmailJob`.""" wrapper = MailWrapper() body_parts = [self.body, wrapper.format(rationale)] if '\n-- ' not in self.body: body_parts.insert(1, '-- ') return '\n'.join(body_parts)
def notify_specification_subscription_modified(specsub, event): """Notify a subscriber to a blueprint that their subscription has changed. """ user = IPerson(event.user) spec = specsub.specification person = specsub.person # Only send a notification if the # subscription changed by someone else. if person == user: return subject = specification_notification_subject(spec) if specsub.essential: specsub_type = 'Participation essential' else: specsub_type = 'Participation non-essential' mailwrapper = MailWrapper(width=72) body = mailwrapper.format( 'Your subscription to the blueprint ' '%(blueprint_name)s - %(blueprint_title)s ' 'has changed to [%(specsub_type)s].\n\n' '--\n %(blueprint_url)s' % {'blueprint_name': spec.name, 'blueprint_title': spec.title, 'specsub_type': specsub_type, 'blueprint_url': canonical_url(spec)}) for address in get_contact_email_addresses(person): simple_sendmail_from_person(user, address, subject, body)
def build_message(text): mailwrapper = MailWrapper(width=72) text = mailwrapper.format(text) message = MIMEText(text.encode('utf-8'), 'plain', 'utf-8') # This is redundant and makes the template noisy del message['MIME-Version'] return message
def sendHelpEmail(self, to_address): """Send usage help to `to_address`.""" # Get the help text (formatted as MoinMoin markup) help_text = get_email_template('help.txt', app='bugs') help_text = reformat_wiki_text(help_text) # Wrap text mailwrapper = MailWrapper(width=72) help_text = mailwrapper.format(help_text) simple_sendmail('*****@*****.**', to_address, 'Launchpad Bug Tracker Email Interface Help', help_text)
def sendHelpEmail(self, to_address): """Send usage help to `to_address`.""" # Get the help text (formatted as MoinMoin markup) help_text = get_email_template('help.txt', app='bugs') help_text = reformat_wiki_text(help_text) # Wrap text mailwrapper = MailWrapper(width=72) help_text = mailwrapper.format(help_text) simple_sendmail( '*****@*****.**', to_address, 'Launchpad Bug Tracker Email Interface Help', help_text)
def send_process_error_notification(to_address, subject, error_msg, original_msg, failing_command=None, max_return_size=MAX_RETURN_MESSAGE_SIZE): """Send a mail about an error occurring while using the email interface. Tells the user that an error was encountered while processing his request and attaches the original email which caused the error to happen. The original message will be truncated to max_return_size bytes. :to_address: The address to send the notification to. :subject: The subject of the notification. :error_msg: The error message that explains the error. :original_msg: The original message sent by the user. :failing_command: The command that caused the error to happen. :max_return_size: The maximum size returned for the original message. """ if isinstance(failing_command, list): failing_commands = failing_command elif failing_command is None: failing_commands = [] else: failing_commands = [failing_command] failed_commands_information = '' if len(failing_commands) > 0: failed_commands_information = 'Failing command:' for failing_command in failing_commands: failed_commands_information += '\n %s' % str(failing_command) body = get_email_template( 'email-processing-error.txt', app='services/mail') % { 'failed_command_information': failed_commands_information, 'error_msg': error_msg } mailwrapper = MailWrapper(width=72) body = mailwrapper.format(body) error_part = MIMEText(body.encode('utf-8'), 'plain', 'utf-8') msg = MIMEMultipart() msg['To'] = to_address msg['From'] = get_bugmail_error_address() msg['Subject'] = subject msg.attach(error_part) original_msg_str = str(original_msg) if len(original_msg_str) > max_return_size: truncated_msg_str = original_msg_str[:max_return_size] original_msg = message_from_string(truncated_msg_str) msg.attach(MIMEMessage(original_msg)) sendmail(msg)
def send_process_error_notification( to_address, subject, error_msg, original_msg, failing_command=None, max_return_size=MAX_RETURN_MESSAGE_SIZE ): """Send a mail about an error occurring while using the email interface. Tells the user that an error was encountered while processing his request and attaches the original email which caused the error to happen. The original message will be truncated to max_return_size bytes. :to_address: The address to send the notification to. :subject: The subject of the notification. :error_msg: The error message that explains the error. :original_msg: The original message sent by the user. :failing_command: The command that caused the error to happen. :max_return_size: The maximum size returned for the original message. """ if isinstance(failing_command, list): failing_commands = failing_command elif failing_command is None: failing_commands = [] else: failing_commands = [failing_command] failed_commands_information = "" if len(failing_commands) > 0: failed_commands_information = "Failing command:" for failing_command in failing_commands: failed_commands_information += "\n %s" % str(failing_command) body = get_email_template("email-processing-error.txt", app="services/mail") % { "failed_command_information": failed_commands_information, "error_msg": error_msg, } mailwrapper = MailWrapper(width=72) body = mailwrapper.format(body) error_part = MIMEText(body.encode("utf-8"), "plain", "utf-8") msg = MIMEMultipart() msg["To"] = to_address msg["From"] = get_bugmail_error_address() msg["Subject"] = subject msg.attach(error_part) original_msg_str = str(original_msg) if len(original_msg_str) > max_return_size: truncated_msg_str = original_msg_str[:max_return_size] original_msg = message_from_string(truncated_msg_str) msg.attach(MIMEMessage(original_msg)) sendmail(msg)
def notify_message_held(message_approval, event): """Send a notification of a message hold to all team administrators.""" message_details = getAdapter(message_approval, IHeldMessageDetails) team = message_approval.mailing_list.team from_address = format_address(team.displayname, config.canonical.noreply_from_address) subject = ('New mailing list message requiring approval for %s' % team.displayname) template = get_email_template('new-held-message.txt', app='registry') # Most of the replacements are the same for everyone. replacements = { 'subject': message_details.subject, 'author_name': message_details.author.displayname, 'author_url': canonical_url(message_details.author), 'date': message_details.date, 'message_id': message_details.message_id, 'review_url': '%s/+mailinglist-moderate' % canonical_url(team), 'team': team.displayname, } # Don't wrap the paragraph with the url. def wrap_function(paragraph): return (paragraph.startswith('http:') or paragraph.startswith('https:')) # Send one message to every team administrator. person_set = getUtility(IPersonSet) for address in team.getTeamAdminsEmailAddresses(): user = person_set.getByEmail(address) replacements['user'] = user.displayname body = MailWrapper(72).format(template % replacements, force_wrap=True, wrap_func=wrap_function) simple_sendmail(from_address, address, subject, body)
def notify_specification_subscription_created(specsub, event): """Notify a user that they have been subscribed to a blueprint.""" user = IPerson(event.user) spec = specsub.specification person = specsub.person subject = specification_notification_subject(spec) mailwrapper = MailWrapper(width=72) body = mailwrapper.format( 'You are now subscribed to the blueprint ' '%(blueprint_name)s - %(blueprint_title)s.\n\n' '-- \n%(blueprint_url)s' % {'blueprint_name': spec.name, 'blueprint_title': spec.title, 'blueprint_url': canonical_url(spec)}) for address in get_contact_email_addresses(person): simple_sendmail_from_person(user, address, subject, body)
def initialize(self): """Save the old question for comparison. It also set the new_message attribute if a new message was added. """ self.old_question = self.event.object_before_modification new_messages = set(self.question.messages).difference( self.old_question.messages) assert len(new_messages) <= 1, ( "There shouldn't be more than one message for a " "notification.") if new_messages: self.new_message = new_messages.pop() else: self.new_message = None self.wrapper = MailWrapper()
def notify_new_ppa_subscription(subscription, event): """Notification that a new PPA subscription can be activated.""" non_active_subscribers = subscription.getNonActiveSubscribers() archive = subscription.archive # We don't send notification emails for some PPAs, particularly those that # are purchased via the Software Centre, so that its users do not have to # learn about Launchpad. if archive.suppress_subscription_notifications: return registrant_name = subscription.registrant.displayname ppa_displayname = archive.displayname ppa_reference = "ppa:%s/%s" % ( archive.owner.name, archive.name) ppa_description = archive.description subject = 'PPA access granted for ' + ppa_displayname template = get_email_template('ppa-subscription-new.txt', app='soyuz') for person, preferred_email in non_active_subscribers: to_address = [preferred_email.email] root = getUtility(ILaunchpadRoot) recipient_subscriptions_url = "%s~/+archivesubscriptions" % ( canonical_url(root)) description_blurb = '.' if ppa_description is not None and ppa_description != '': description_blurb = ( ' and has the following description:\n\n%s' % ppa_description) replacements = { 'recipient_name': person.displayname, 'registrant_name': registrant_name, 'registrant_profile_url': canonical_url(subscription.registrant), 'ppa_displayname': ppa_displayname, 'ppa_reference': ppa_reference, 'ppa_description_blurb': description_blurb, 'recipient_subscriptions_url': recipient_subscriptions_url, } body = MailWrapper(72).format(template % replacements, force_wrap=True) from_address = format_address( registrant_name, config.canonical.noreply_from_address) headers = { 'Sender': config.canonical.bounce_address, } # If the registrant has a preferred email, then use it for the # Reply-To. if subscription.registrant.preferredemail: headers['Reply-To'] = format_address( registrant_name, subscription.registrant.preferredemail.email) simple_sendmail(from_address, to_address, subject, body, headers)
def _getBody(self, email, recipient): """Return the complete body to use for this email.""" template = get_email_template(self._getTemplateName(email, recipient), app=self.app) params = self._getTemplateParams(email, recipient) body = template % params if self._wrap: body = MailWrapper().format(body, force_wrap=self._force_wrap) + "\n" footer = self._getFooter(email, recipient, params) if footer is not None: body = append_footer(body, footer) return body
def get_unified_diff(old_text, new_text, text_width): r"""Return a unified diff of the two texts. Before the diff is produced, the texts are wrapped to the given text width. >>> print get_unified_diff( ... 'Some text\nAnother line\n',get_un ... 'Some more text\nAnother line\n', ... text_width=72) - Some text + Some more text Another line """ mailwrapper = MailWrapper(width=72) old_text_wrapped = mailwrapper.format(old_text or '') new_text_wrapped = mailwrapper.format(new_text or '') lines_of_context = len(old_text_wrapped.splitlines()) text_diff = unified_diff( old_text_wrapped.splitlines(), new_text_wrapped.splitlines(), n=lines_of_context) # Remove the diff header, which consists of the first three # lines. text_diff = list(text_diff)[3:] # Let's simplify the diff output by removing the helper lines, # which begin with '?'. text_diff = [ diff_line for diff_line in text_diff if not diff_line.startswith('?')] # Add a whitespace between the +/- and the text line. text_diff = [ re.sub('^([\+\- ])(.*)', r'\1 \2', line) for line in text_diff] text_diff = '\n'.join(text_diff) return text_diff
def send_team_email(from_addr, address, subject, template, replacements, rationale, headers=None): """Send a team message with a rationale.""" if headers is None: headers = {} body = MailWrapper().format(template % replacements, force_wrap=True) footer = "-- \n%s" % rationale message = '%s\n\n%s' % (body, footer) simple_sendmail(from_addr, address, subject, message, headers)
def initialize(self): """Save the old question for comparison. It also set the new_message attribute if a new message was added. """ self.old_question = self.event.object_before_modification new_messages = set( self.question.messages).difference(self.old_question.messages) assert len(new_messages) <= 1, ( "There shouldn't be more than one message for a " "notification.") if new_messages: self.new_message = new_messages.pop() else: self.new_message = None self.wrapper = MailWrapper()
def getBodyAndHeaders(self, email_template, address, reply_to=None): """See `IProductNotificationJob`.""" reason, rationale = self.recipients.getReason(address) maintainer = self.recipients._emailToPerson[address] message_data = dict(self.message_data) message_data['user_name'] = maintainer.name message_data['user_displayname'] = maintainer.displayname raw_body = email_template % message_data raw_body += '\n\n-- \n%s' % reason body = MailWrapper().format(raw_body, force_wrap=True) headers = { 'X-Launchpad-Project': '%(product_displayname)s (%(product_name)s)' % message_data, 'X-Launchpad-Message-Rationale': rationale, } if reply_to is not None: headers['Reply-To'] = reply_to return body, headers
def _importEntry(self, entry): """Perform the import of one entry, and notify the uploader.""" target = entry.import_into self.logger.info('Importing: %s' % target.title) (mail_subject, mail_body) = target.importFromQueue(entry, self.logger) if mail_subject is not None and self._shouldNotify(entry.importer): # A `mail_subject` of None indicates that there # is no notification worth sending out. from_email = config.rosetta.notification_address katie = getUtility(ILaunchpadCelebrities).katie if entry.importer == katie: # Email import state to Debian imports email. to_email = None else: to_email = get_contact_email_addresses(entry.importer) if to_email: text = MailWrapper().format(mail_body) simple_sendmail(from_email, to_email, mail_subject, text)
def notify_invitation_to_join_team(event): """Notify team admins that the team has been invited to join another team. The notification will include a link to a page in which any team admin can accept the invitation. XXX: Guilherme Salgado 2007-05-08: At some point we may want to extend this functionality to allow invites to be sent to users as well, but for now we only use it for teams. """ member = event.member assert member.is_team team = event.team membership = getUtility(ITeamMembershipSet).getByPersonAndTeam( member, team) assert membership is not None reviewer = membership.proposed_by admin_addrs = member.getTeamAdminsEmailAddresses() from_addr = format_address(team.displayname, config.canonical.noreply_from_address) subject = 'Invitation for %s to join' % member.name templatename = 'membership-invitation.txt' template = get_email_template(templatename, app='registry') replacements = { 'reviewer': '%s (%s)' % (reviewer.displayname, reviewer.name), 'member': '%s (%s)' % (member.displayname, member.name), 'team': '%s (%s)' % (team.displayname, team.name), 'team_url': canonical_url(team), 'membership_invitations_url': "%s/+invitation/%s" % (canonical_url(member), team.name) } for address in admin_addrs: recipient = getUtility(IPersonSet).getByEmail(address) replacements['recipient_name'] = recipient.displayname msg = MailWrapper().format(template % replacements, force_wrap=True) simple_sendmail(from_addr, address, subject, msg)
def sendCancellationEmail(self, token): """Send an email to the person whose subscription was cancelled.""" if token.archive.suppress_subscription_notifications: # Don't send an email if they should be suppresed for the # archive return send_to_person = token.person ppa_name = token.archive.displayname ppa_owner_url = canonical_url(token.archive.owner) subject = "PPA access cancelled for %s" % ppa_name template = get_email_template( "ppa-subscription-cancelled.txt", app='soyuz') assert not send_to_person.is_team, ( "Token.person is a team, it should always be individuals.") if send_to_person.preferredemail is None: # The person has no preferred email set, so we don't # email them. return to_address = [send_to_person.preferredemail.email] replacements = { 'recipient_name': send_to_person.displayname, 'ppa_name': ppa_name, 'ppa_owner_url': ppa_owner_url, } body = MailWrapper(72).format( template % replacements, force_wrap=True) from_address = format_address( ppa_name, config.canonical.noreply_from_address) headers = { 'Sender': config.canonical.bounce_address, } simple_sendmail(from_address, to_address, subject, body, headers)
def sendSelfRenewalNotification(self): """See `ITeamMembership`.""" team = self.team member = self.person assert team.renewal_policy == TeamMembershipRenewalPolicy.ONDEMAND from_addr = format_address( team.displayname, config.canonical.noreply_from_address) replacements = {'member_name': member.unique_displayname, 'team_name': team.unique_displayname, 'team_url': canonical_url(team), 'dateexpires': self.dateexpires.strftime('%Y-%m-%d')} subject = '%s extended their membership' % member.name template = get_email_template( 'membership-member-renewed.txt', app='registry') admins_addrs = self.team.getTeamAdminsEmailAddresses() for address in admins_addrs: recipient = getUtility(IPersonSet).getByEmail(address) replacements['recipient_name'] = recipient.displayname msg = MailWrapper().format( template % replacements, force_wrap=True) simple_sendmail(from_addr, address, subject, msg)
def notify_mailinglist_activated(mailinglist, event): """Notification that a mailing list is available. All active members of a team and its subteams receive notification when the team's mailing list is available. """ # We will use the setting of the date_activated field as a hint # that this list is new, and that noboby has subscribed yet. See # `MailingList.transitionToStatus()` for the details. old_date = event.object_before_modification.date_activated new_date = event.object.date_activated list_looks_new = old_date is None and new_date is not None if not (list_looks_new and mailinglist.is_usable): return team = mailinglist.team from_address = format_address(team.displayname, config.canonical.noreply_from_address) headers = {} subject = "New Mailing List for %s" % team.displayname template = get_email_template('new-mailing-list.txt', app='registry') editemails_url = '%s/+editmailinglists' for person in team.allmembers: if person.is_team or person.preferredemail is None: # This is either a team or a person without a preferred email, so # don't send a notification. continue to_address = [str(person.preferredemail.email)] replacements = { 'user': person.displayname, 'team_displayname': team.displayname, 'team_name': team.name, 'team_url': canonical_url(team), 'subscribe_url': editemails_url % canonical_url(person), } body = MailWrapper(72).format(template % replacements, force_wrap=True) simple_sendmail(from_address, to_address, subject, body, headers)
def run(self): self.logger.info("Starting verification of POFile stats at id %d" % self.start_at_id) loop = Verifier(self.transaction, self.logger, self.start_at_id) # Since the script can run for a long time, our deployment # process might remove the Launchpad tree the script was run # from, thus the script failing to find the email template # if it was attempted after DBLoopTuner run is completed. # See bug #811447 for OOPS we used to get then. template = get_email_template('pofile-stats.txt', 'translations') # Each iteration of our loop collects all statistics first, before # modifying any rows in the database. With any locks on the database # acquired only at the very end of the iteration, we can afford to # make relatively long, low-overhead iterations without disrupting # application response times. iteration_duration = ( config.rosetta_pofile_stats.looptuner_iteration_duration) DBLoopTuner(loop, iteration_duration).run() if loop.total_incorrect > 0 or loop.total_exceptions > 0: # Not all statistics were correct, or there were failures while # checking them. Email the admins. message = template % { 'exceptions': loop.total_exceptions, 'errors': loop.total_incorrect, 'total': loop.total_checked } simple_sendmail(from_addr=config.canonical.noreply_from_address, to_addrs=[config.launchpad.errors_address], subject="POFile statistics errors", body=MailWrapper().format(message)) self.transaction.commit() self.logger.info("Done.")
def notify_specification_modified(spec, event): """Notify the related people that a specification has been modifed.""" user = IPerson(event.user) spec_delta = spec.getDelta(event.object_before_modification, user) if spec_delta is None: # XXX: Bjorn Tillenius 2006-03-08: # Ideally, if an IObjectModifiedEvent event is generated, # spec_delta shouldn't be None. I'm not confident that we # have enough test yet to assert this, though. return subject = specification_notification_subject(spec) indent = ' ' * 4 info_lines = [] if spec_delta.name: info_lines.append('%sName: %s => %s' % ( indent, spec_delta.name['old'], spec_delta.name['new'])) for dbitem_name in ('definition_status', 'priority'): title = ISpecification[dbitem_name].title assert ISpecification[dbitem_name].required, ( "The mail notification assumes %s can't be None" % dbitem_name) dbitem_delta = getattr(spec_delta, dbitem_name) if dbitem_delta is not None: old_item = dbitem_delta['old'] new_item = dbitem_delta['new'] info_lines.append("%s%s: %s => %s" % ( indent, title, old_item.title, new_item.title)) for person_attrname in ('approver', 'assignee', 'drafter'): title = ISpecification[person_attrname].title person_delta = getattr(spec_delta, person_attrname) if person_delta is not None: old_person = person_delta['old'] if old_person is None: old_value = "(none)" else: old_value = old_person.displayname new_person = person_delta['new'] if new_person is None: new_value = "(none)" else: new_value = new_person.displayname info_lines.append( "%s%s: %s => %s" % (indent, title, old_value, new_value)) mail_wrapper = MailWrapper(width=72) if spec_delta.whiteboard is not None: if info_lines: info_lines.append('') whiteboard_delta = spec_delta.whiteboard if whiteboard_delta['old'] is None: info_lines.append('Whiteboard set to:') info_lines.append(mail_wrapper.format(whiteboard_delta['new'])) else: whiteboard_diff = get_unified_diff( whiteboard_delta['old'], whiteboard_delta['new'], 72) info_lines.append('Whiteboard changed:') info_lines.append(whiteboard_diff) if spec_delta.workitems_text is not None: if info_lines: info_lines.append('') workitems_delta = spec_delta.workitems_text if workitems_delta['old'] is '': info_lines.append('Work items set to:') info_lines.append(mail_wrapper.format(workitems_delta['new'])) else: workitems_diff = get_unified_diff( workitems_delta['old'], workitems_delta['new'], 72) info_lines.append('Work items changed:') info_lines.append(workitems_diff) if not info_lines: # The specification was modified, but we don't yet support # sending notification for the change. return body = get_email_template('specification-modified.txt', 'blueprints') % { 'editor': user.displayname, 'info_fields': '\n'.join(info_lines), 'spec_title': spec.title, 'spec_url': canonical_url(spec)} for address in spec.notificationRecipientAddresses(): simple_sendmail_from_person(user, address, subject, body)
class QuestionModifiedDefaultNotification(QuestionNotification): """Base implementation of a notification when a question is modified.""" recipient_set = QuestionRecipientSet.SUBSCRIBER # Email template used to render the body. body_template = "question-modified-notification.txt" def initialize(self): """Save the old question for comparison. It also set the new_message attribute if a new message was added. """ self.old_question = self.event.object_before_modification new_messages = set(self.question.messages).difference( self.old_question.messages) assert len(new_messages) <= 1, ( "There shouldn't be more than one message for a " "notification.") if new_messages: self.new_message = new_messages.pop() else: self.new_message = None self.wrapper = MailWrapper() @cachedproperty def metadata_changes_text(self): """Textual representation of the changes to the question metadata.""" question = self.question old_question = self.old_question indent = 4 * ' ' info_fields = [] if question.status != old_question.status: info_fields.append( indent + 'Status: %s => %s' % (old_question.status.title, question.status.title)) if question.target != old_question.target: info_fields.append( indent + 'Project: %s => %s' % (old_question.target.displayname, question.target.displayname)) if question.assignee != old_question.assignee: if old_question.assignee is None: old_assignee = None else: old_assignee = old_question.assignee.displayname if question.assignee is None: assignee = None else: assignee = question.assignee.displayname info_fields.append(indent + 'Assignee: %s => %s' % (old_assignee, assignee)) old_bugs = set(old_question.bugs) bugs = set(question.bugs) for linked_bug in bugs.difference(old_bugs): info_fields.append(indent + 'Linked to bug: #%s\n' % linked_bug.id + indent + '%s\n' % canonical_url(linked_bug) + indent + '"%s"' % linked_bug.title) for unlinked_bug in old_bugs.difference(bugs): info_fields.append(indent + 'Removed link to bug: #%s\n' % unlinked_bug.id + indent + '%s\n' % canonical_url(unlinked_bug) + indent + '"%s"' % unlinked_bug.title) if question.faq != old_question.faq: if question.faq is None: info_fields.append(indent + 'Related FAQ was removed:\n' + indent + old_question.faq.title + '\n' + indent + canonical_url(old_question.faq)) else: info_fields.append(indent + 'Related FAQ set to:\n' + indent + question.faq.title + '\n' + indent + canonical_url(question.faq)) if question.title != old_question.title: info_fields.append('Summary changed to:\n%s' % question.title) if question.description != old_question.description: info_fields.append('Description changed to:\n%s' % (self.wrapper.format(question.description))) question_changes = '\n\n'.join(info_fields) return question_changes def getSubject(self): """The reply subject line.""" line = super(QuestionModifiedDefaultNotification, self).getSubject() return 'Re: %s' % line def getHeaders(self): """Add a References header.""" headers = QuestionNotification.getHeaders(self) if self.new_message: # XXX flacoste 2007-02-02 bug=83846: # The first message cannot contain a References # because we don't create a Message instance for the # question description, so we don't have a Message-ID. messages = list(self.question.messages) assert self.new_message in messages, ( "Question %s: message id %s not in %s." % (self.question.id, self.new_message.id, [m.id for m in messages])) index = messages.index(self.new_message) if index > 0: headers['References'] = (self.question.messages[index - 1].rfc822msgid) return headers def shouldNotify(self): """Only send a notification when a message was added or some metadata was changed. """ return self.new_message or self.metadata_changes_text def getBody(self): """See QuestionNotification.""" body = self.metadata_changes_text replacements = dict(question_id=self.question.id, target_name=self.question.target.displayname, question_url=canonical_url(self.question)) if self.new_message: if body: body += '\n\n' body += self.getNewMessageText() replacements['new_message_id'] = list( self.question.messages).index(self.new_message) replacements['body'] = body return get_email_template(self.body_template) % replacements # Header template used when a new message is added to the question. action_header_template = { QuestionAction.REQUESTINFO: '%(person)s requested more information:', QuestionAction.CONFIRM: '%(person)s confirmed that the question is solved:', QuestionAction.COMMENT: '%(person)s posted a new comment:', QuestionAction.GIVEINFO: '%(person)s gave more information on the question:', QuestionAction.REOPEN: '%(person)s is still having a problem:', QuestionAction.ANSWER: '%(person)s proposed the following answer:', QuestionAction.EXPIRE: '%(person)s expired the question:', QuestionAction.REJECT: '%(person)s rejected the question:', QuestionAction.SETSTATUS: '%(person)s changed the question status:', } def getNewMessageText(self): """Should return the notification text related to a new message.""" if not self.new_message: return '' header = self.action_header_template.get( self.new_message.action, '%(person)s posted a new message:') % { 'person': self.new_message.owner.displayname } return '\n'.join( [header, self.wrapper.format(self.new_message.text_contents)])
def generate_bug_add_email(bug, new_recipients=False, reason=None, subscribed_by=None, event_creator=None): """Generate a new bug notification from the given IBug. If new_recipients is supplied we generate a notification explaining that the new recipients have been subscribed to the bug. Otherwise it's just a notification of a new bug report. """ subject = u"[Bug %d] [NEW] %s" % (bug.id, bug.title) contents = '' if bug.private: # This is a confidential bug. visibility = u"Private" else: # This is a public bug. visibility = u"Public" if bug.security_related: visibility += ' security' contents += '*** This bug is a security vulnerability ***\n\n' bug_info = [] # Add information about the affected upstreams and packages. for bugtask in bug.bugtasks: bug_info.append(u"** Affects: %s" % bugtask.bugtargetname) bug_info.append(u" Importance: %s" % bugtask.importance.title) if bugtask.assignee: # There's a person assigned to fix this task, so show that # information too. bug_info.append( u" Assignee: %s" % bugtask.assignee.unique_displayname) bug_info.append(u" Status: %s\n" % bugtask.status.title) if bug.tags: bug_info.append('\n** Tags: %s' % ' '.join(bug.tags)) mailwrapper = MailWrapper(width=72) content_substitutions = { 'visibility': visibility, 'bug_url': canonical_url(bug), 'bug_info': "\n".join(bug_info), 'bug_title': bug.title, 'description': mailwrapper.format(bug.description), 'notification_rationale': reason, } if new_recipients: if "assignee" in reason and event_creator is not None: if event_creator == bugtask.assignee: contents += ( "You have assigned this bug to yourself for %(target)s") else: contents += ( "%(assigner)s has assigned this bug to you for " + "%(target)s") content_substitutions['assigner'] = ( event_creator.unique_displayname) content_substitutions['target'] = bugtask.target.displayname else: contents += "You have been subscribed to a %(visibility)s bug" if subscribed_by is not None: contents += " by %(subscribed_by)s" content_substitutions['subscribed_by'] = ( subscribed_by.unique_displayname) contents += (":\n\n" "%(description)s\n\n%(bug_info)s") # The visibility appears mid-phrase so.. hack hack. content_substitutions['visibility'] = visibility.lower() # XXX: kiko, 2007-03-21: # We should really have a centralized way of adding this # footer, but right now we lack a INotificationRecipientSet # for this particular situation. contents += ( "\n-- \n%(bug_title)s\n%(bug_url)s\n%(notification_rationale)s") else: contents += ("%(visibility)s bug reported:\n\n" "%(description)s\n\n%(bug_info)s") contents = contents % content_substitutions contents = contents.rstrip() return (subject, contents)
def send_direct_contact_email( sender_email, recipients_set, subject, body): """Send a direct user-to-user email. :param sender_email: The email address of the sender. :type sender_email: string :param recipients_set: The recipients. :type recipients_set:' A ContactViaWebNotificationSet :param subject: The Subject header. :type subject: unicode :param body: The message body. :type body: unicode :return: The sent message. :rtype: `email.Message.Message` """ # Craft the email message. Start by checking whether the subject and # message bodies are ASCII or not. subject_header = encode(subject) try: body.encode('us-ascii') charset = 'us-ascii' except UnicodeEncodeError: charset = 'utf-8' # Get the sender's real name, encoded as per RFC 2047. person_set = getUtility(IPersonSet) sender = person_set.getByEmail(sender_email) assert sender is not None, 'No person for sender %s' % sender_email sender_name = str(encode(sender.displayname)) # Do a single authorization/quota check for the sender. We consume one # quota credit per contact, not per recipient. authorization = IDirectEmailAuthorization(sender) if not authorization.is_allowed: raise QuotaReachedError(sender.displayname, authorization) # Add the footer as a unicode string, then encode the body if necessary. # This is not entirely optimal if the body has non-ascii characters in it, # since the footer may get garbled in a non-MIME aware mail reader. Who # uses those anyway!? The only alternative is to attach the footer as a # MIME attachment with a us-ascii charset, but that has it's own set of # problems (and user complaints). Email sucks. additions = u'\n'.join([ u'', u'-- ', u'This message was sent from Launchpad by', u'%s (%s)' % (sender_name, canonical_url(sender)), u'%s.', u'For more information see', u'https://help.launchpad.net/YourAccount/ContactingPeople', ]) # Craft and send one message per recipient. mailwrapper = MailWrapper(width=72) message = None for recipient_email, recipient in recipients_set.getRecipientPersons(): recipient_name = str(encode(recipient.displayname)) reason, rational_header = recipients_set.getReason(recipient_email) reason = str(encode(reason)).replace('\n ', '\n') formatted_body = mailwrapper.format(body, force_wrap=True) formatted_body += additions % reason formatted_body = formatted_body.encode(charset) message = MIMEText(formatted_body, _charset=charset) message['From'] = formataddr((sender_name, sender_email)) message['To'] = formataddr((recipient_name, recipient_email)) message['Subject'] = subject_header message['Message-ID'] = make_msgid('launchpad') message['X-Launchpad-Message-Rationale'] = rational_header # Send the message. sendmail(message, bulk=False) # Use the information from the last message sent to record the action # taken. The record will be used to throttle user-to-user emails. if message is not None: authorization.record(message)
def run(self): """See `IMembershipNotificationJob`.""" from lp.services.scripts import log from_addr = format_address( self.team.displayname, config.canonical.noreply_from_address) admin_emails = self.team.getTeamAdminsEmailAddresses() # person might be a self.team, so we can't rely on its preferredemail. self.member_email = get_contact_email_addresses(self.member) # Make sure we don't send the same notification twice to anybody. for email in self.member_email: if email in admin_emails: admin_emails.remove(email) if self.reviewer != self.member: self.reviewer_name = self.reviewer.unique_displayname else: self.reviewer_name = 'the user' if self.last_change_comment: comment = ("\n%s said:\n %s\n" % ( self.reviewer.displayname, self.last_change_comment.strip())) else: comment = "" replacements = { 'member_name': self.member.unique_displayname, 'recipient_name': self.member.displayname, 'team_name': self.team.unique_displayname, 'team_url': canonical_url(self.team), 'old_status': self.old_status.title, 'new_status': self.new_status.title, 'reviewer_name': self.reviewer_name, 'comment': comment} template_name = 'membership-statuschange' subject = ( 'Membership change: %(member)s in %(team)s' % { 'member': self.member.name, 'team': self.team.name, }) if self.new_status == TeamMembershipStatus.EXPIRED: template_name = 'membership-expired' subject = '%s expired from team' % self.member.name elif (self.new_status == TeamMembershipStatus.APPROVED and self.old_status != TeamMembershipStatus.ADMIN): if self.old_status == TeamMembershipStatus.INVITED: subject = ('Invitation to %s accepted by %s' % (self.member.name, self.reviewer.name)) template_name = 'membership-invitation-accepted' elif self.old_status == TeamMembershipStatus.PROPOSED: subject = '%s approved by %s' % ( self.member.name, self.reviewer.name) else: subject = '%s added by %s' % ( self.member.name, self.reviewer.name) elif self.new_status == TeamMembershipStatus.INVITATION_DECLINED: subject = ('Invitation to %s declined by %s' % (self.member.name, self.reviewer.name)) template_name = 'membership-invitation-declined' elif self.new_status == TeamMembershipStatus.DEACTIVATED: subject = '%s deactivated by %s' % ( self.member.name, self.reviewer.name) elif self.new_status == TeamMembershipStatus.ADMIN: subject = '%s made admin by %s' % ( self.member.name, self.reviewer.name) elif self.new_status == TeamMembershipStatus.DECLINED: subject = '%s declined by %s' % ( self.member.name, self.reviewer.name) else: # Use the default template and subject. pass # Must have someone to mail, and be a non-open team (because open # teams are unrestricted, notifications on join/ leave do not help the # admins. if (len(admin_emails) != 0 and self.team.membership_policy != TeamMembershipPolicy.OPEN): admin_template = get_email_template( "%s-bulk.txt" % template_name, app='registry') for address in admin_emails: recipient = getUtility(IPersonSet).getByEmail(address) replacements['recipient_name'] = recipient.displayname msg = MailWrapper().format( admin_template % replacements, force_wrap=True) simple_sendmail(from_addr, address, subject, msg) # The self.member can be a self.self.team without any # self.members, and in this case we won't have a single email # address to send this notification to. if self.member_email and self.reviewer != self.member: if self.member.is_team: template = '%s-bulk.txt' % template_name else: template = '%s-personal.txt' % template_name self.member_template = get_email_template( template, app='registry') for address in self.member_email: recipient = getUtility(IPersonSet).getByEmail(address) replacements['recipient_name'] = recipient.displayname msg = MailWrapper().format( self.member_template % replacements, force_wrap=True) simple_sendmail(from_addr, address, subject, msg) log.debug('MembershipNotificationJob sent email')
def getReason(self): """See `RecipientReason`.""" return MailWrapper(width=72).format( super(PackageUploadRecipientReason, self).getReason())
class QuestionModifiedDefaultNotification(QuestionNotification): """Base implementation of a notification when a question is modified.""" recipient_set = QuestionRecipientSet.SUBSCRIBER # Email template used to render the body. body_template = "question-modified-notification.txt" def initialize(self): """Save the old question for comparison. It also set the new_message attribute if a new message was added. """ self.old_question = self.event.object_before_modification new_messages = set( self.question.messages).difference(self.old_question.messages) assert len(new_messages) <= 1, ( "There shouldn't be more than one message for a " "notification.") if new_messages: self.new_message = new_messages.pop() else: self.new_message = None self.wrapper = MailWrapper() @cachedproperty def metadata_changes_text(self): """Textual representation of the changes to the question metadata.""" question = self.question old_question = self.old_question indent = 4 * ' ' info_fields = [] if question.status != old_question.status: info_fields.append(indent + 'Status: %s => %s' % ( old_question.status.title, question.status.title)) if question.target != old_question.target: info_fields.append( indent + 'Project: %s => %s' % ( old_question.target.displayname, question.target.displayname)) if question.assignee != old_question.assignee: if old_question.assignee is None: old_assignee = None else: old_assignee = old_question.assignee.displayname if question.assignee is None: assignee = None else: assignee = question.assignee.displayname info_fields.append(indent + 'Assignee: %s => %s' % ( old_assignee, assignee)) old_bugs = set(old_question.bugs) bugs = set(question.bugs) for linked_bug in bugs.difference(old_bugs): info_fields.append( indent + 'Linked to bug: #%s\n' % linked_bug.id + indent + '%s\n' % canonical_url(linked_bug) + indent + '"%s"' % linked_bug.title) for unlinked_bug in old_bugs.difference(bugs): info_fields.append( indent + 'Removed link to bug: #%s\n' % unlinked_bug.id + indent + '%s\n' % canonical_url(unlinked_bug) + indent + '"%s"' % unlinked_bug.title) if question.faq != old_question.faq: if question.faq is None: info_fields.append( indent + 'Related FAQ was removed:\n' + indent + old_question.faq.title + '\n' + indent + canonical_url(old_question.faq)) else: info_fields.append( indent + 'Related FAQ set to:\n' + indent + question.faq.title + '\n' + indent + canonical_url(question.faq)) if question.title != old_question.title: info_fields.append('Summary changed to:\n%s' % question.title) if question.description != old_question.description: info_fields.append( 'Description changed to:\n%s' % ( self.wrapper.format(question.description))) question_changes = '\n\n'.join(info_fields) return question_changes def getSubject(self): """The reply subject line.""" line = super(QuestionModifiedDefaultNotification, self).getSubject() return 'Re: %s' % line def getHeaders(self): """Add a References header.""" headers = QuestionNotification.getHeaders(self) if self.new_message: # XXX flacoste 2007-02-02 bug=83846: # The first message cannot contain a References # because we don't create a Message instance for the # question description, so we don't have a Message-ID. messages = list(self.question.messages) assert self.new_message in messages, ( "Question %s: message id %s not in %s." % ( self.question.id, self.new_message.id, [m.id for m in messages])) index = messages.index(self.new_message) if index > 0: headers['References'] = ( self.question.messages[index - 1].rfc822msgid) return headers def shouldNotify(self): """Only send a notification when a message was added or some metadata was changed. """ return self.new_message or self.metadata_changes_text def getBody(self): """See QuestionNotification.""" body = self.metadata_changes_text replacements = dict( question_id=self.question.id, target_name=self.question.target.displayname, question_url=canonical_url(self.question)) if self.new_message: if body: body += '\n\n' body += self.getNewMessageText() replacements['new_message_id'] = list( self.question.messages).index(self.new_message) replacements['body'] = body return get_email_template(self.body_template) % replacements # Header template used when a new message is added to the question. action_header_template = { QuestionAction.REQUESTINFO: '%(person)s requested more information:', QuestionAction.CONFIRM: '%(person)s confirmed that the question is solved:', QuestionAction.COMMENT: '%(person)s posted a new comment:', QuestionAction.GIVEINFO: '%(person)s gave more information on the question:', QuestionAction.REOPEN: '%(person)s is still having a problem:', QuestionAction.ANSWER: '%(person)s proposed the following answer:', QuestionAction.EXPIRE: '%(person)s expired the question:', QuestionAction.REJECT: '%(person)s rejected the question:', QuestionAction.SETSTATUS: '%(person)s changed the question status:', } def getNewMessageText(self): """Should return the notification text related to a new message.""" if not self.new_message: return '' header = self.action_header_template.get( self.new_message.action, '%(person)s posted a new message:') % { 'person': self.new_message.owner.displayname} return '\n'.join([ header, self.wrapper.format(self.new_message.text_contents)])
def send_direct_contact_email(sender_email, recipients_set, person_or_team, subject, body): """Send a direct user-to-user email. :param sender_email: The email address of the sender. :type sender_email: string :param recipients_set: The recipients. :type recipients_set: `ContactViaWebNotificationSet` :param person_or_team: The party that is the context of the email. :type person_or_team: `IPerson` :param subject: The Subject header. :type subject: unicode :param body: The message body. :type body: unicode :return: The sent message. :rtype: `email.message.Message` """ # Craft the email message. Start by checking whether the subject and # message bodies are ASCII or not. subject_header = encode(subject) try: body.encode('us-ascii') charset = 'us-ascii' except UnicodeEncodeError: charset = 'utf-8' # Get the sender's real name, encoded as per RFC 2047. person_set = getUtility(IPersonSet) sender = person_set.getByEmail(sender_email) assert sender is not None, 'No person for sender %s' % sender_email sender_name = str(encode(sender.displayname)) # Do a single authorization/quota check for the sender. We consume one # quota credit per contact, not per recipient. authorization = IDirectEmailAuthorization(sender) if not authorization.is_allowed: raise QuotaReachedError(sender.displayname, authorization) # Add the footer as a unicode string, then encode the body if necessary. # This is not entirely optimal if the body has non-ascii characters in it, # since the footer may get garbled in a non-MIME aware mail reader. Who # uses those anyway!? The only alternative is to attach the footer as a # MIME attachment with a us-ascii charset, but that has it's own set of # problems (and user complaints). Email sucks. additions = u'\n'.join([ u'', u'-- ', u'This message was sent from Launchpad by', u'%s (%s)' % (sender_name, canonical_url(sender)), u'%s.', u'For more information see', u'https://help.launchpad.net/YourAccount/ContactingPeople', ]) # Craft and send one message per recipient. mailwrapper = MailWrapper(width=72) message = None for recipient_email, recipient in recipients_set.getRecipientPersons(): recipient_name = str(encode(recipient.displayname)) reason, rationale_header = recipients_set.getReason(recipient_email) reason = str(encode(reason)).replace('\n ', '\n') formatted_body = mailwrapper.format(body, force_wrap=True) formatted_body += additions % reason formatted_body = formatted_body.encode(charset) message = MIMEText(formatted_body, _charset=charset) message['From'] = formataddr((sender_name, sender_email)) message['To'] = formataddr((recipient_name, recipient_email)) message['Subject'] = subject_header message['Message-ID'] = make_msgid('launchpad') message['X-Launchpad-Message-Rationale'] = rationale_header message['X-Launchpad-Message-For'] = person_or_team.name # Send the message. sendmail(message, bulk=False) # Use the information from the last message sent to record the action # taken. The record will be used to throttle user-to-user emails. if message is not None: authorization.record(message)
def construct_email_notifications(bug_notifications): """Construct an email from a list of related bug notifications. The person and bug has to be the same for all notifications, and there can be only one comment. """ first_notification = bug_notifications[0] bug = first_notification.bug actor = first_notification.message.owner subject = first_notification.message.subject comment = None references = [] text_notifications = [] old_values = {} new_values = {} for notification in bug_notifications: assert notification.bug == bug, bug.id assert notification.message.owner == actor, actor.id if notification.is_comment: assert comment is None, "Only one of the notifications is allowed to be a comment." comment = notification.message else: key = get_activity_key(notification) if key is not None: if key not in old_values: old_values[key] = notification.activity.oldvalue new_values[key] = notification.activity.newvalue recipients = {} filtered_notifications = [] omitted_notifications = [] for notification in bug_notifications: key = get_activity_key(notification) if notification.is_comment or key is None or key == "removed_subscriber" or old_values[key] != new_values[key]: # We will report this notification. filtered_notifications.append(notification) for subscription_source in notification.recipients: for recipient in get_recipients(subscription_source.person): # The subscription_source.person may be a person or a # team. The get_recipients function gives us everyone # who should actually get an email for that person. # If subscription_source.person is a person or a team # with a preferred email address, then the people to # be emailed will only be subscription_source.person. # However, if it is a team without a preferred email # address, then this list will be the people and teams # that comprise the team, transitively, stopping the walk # at each person and at each team with a preferred email # address. sources_for_person = recipients.get(recipient) if sources_for_person is None: sources_for_person = [] recipients[recipient] = sources_for_person sources_for_person.append(subscription_source) else: omitted_notifications.append(notification) # If the actor does not want self-generated bug notifications, remove the # actor now. if not actor.selfgenerated_bugnotifications: recipients.pop(actor, None) if bug.duplicateof is not None: text_notifications.append( "*** This bug is a duplicate of bug %d ***\n %s" % (bug.duplicateof.id, canonical_url(bug.duplicateof)) ) if comment is not None: if comment == bug.initial_message: subject, text = generate_bug_add_email(bug) else: text = comment.text_contents text_notifications.append(text) msgid = comment.rfc822msgid email_date = comment.datecreated reference = comment.parent while reference is not None: references.insert(0, reference.rfc822msgid) reference = reference.parent else: msgid = first_notification.message.rfc822msgid email_date = first_notification.message.datecreated for notification in filtered_notifications: if notification.message == comment: # Comments were just handled in the previous if block. continue text = notification.message.text_contents.rstrip() text_notifications.append(text) if bug.initial_message.rfc822msgid not in references: # Ensure that references contain the initial message ID references.insert(0, bug.initial_message.rfc822msgid) # At this point we've got the data we need to construct the # messages. Now go ahead and actually do that. messages = [] mail_wrapper = MailWrapper(width=72) content = "\n\n".join(text_notifications) from_address = get_bugmail_from_address(actor, bug) bug_notification_builder = BugNotificationBuilder(bug, actor) recipients = getUtility(IBugNotificationSet).getRecipientFilterData(bug, recipients, filtered_notifications) sorted_recipients = sorted(recipients.items(), key=lambda t: t[0].preferredemail.email) for email_person, data in sorted_recipients: address = str(email_person.preferredemail.email) # Choosing the first source is a bit arbitrary, but it # is simple for the user to understand. We may want to reconsider # this in the future. reason = data["sources"][0].reason_body rationale = data["sources"][0].reason_header if data["filter descriptions"]: # There are some filter descriptions as well. Add them to # the email body. filters_text = u"\nMatching subscriptions: %s" % ", ".join(data["filter descriptions"]) else: filters_text = u"" # In the rare case of a bug with no bugtasks, we can't generate the # subscription management URL so just leave off the subscription # management message entirely. if len(bug.bugtasks): bug_url = canonical_url(bug.bugtasks[0]) notification_url = bug_url + "/+subscriptions" subscriptions_message = "To manage notifications about this bug go to:\n%s" % notification_url else: subscriptions_message = "" data_wrapper = MailWrapper(width=72, indent=" ") body_data = { "content": mail_wrapper.format(content), "bug_title": data_wrapper.format(bug.title), "bug_url": canonical_url(bug), "notification_rationale": mail_wrapper.format(reason), "subscription_filters": filters_text, "subscriptions_message": subscriptions_message, } # If the person we're sending to receives verbose notifications # we include the description and status of the bug in the email # footer. if email_person.verbose_bugnotifications: email_template = "bug-notification-verbose.txt" body_data["bug_description"] = data_wrapper.format(bug.description) status_base = "Status in %s:\n %s" status_strings = [] for bug_task in bug.bugtasks: status_strings.append(status_base % (bug_task.target.title, bug_task.status.title)) body_data["bug_statuses"] = "\n".join(status_strings) else: email_template = "bug-notification.txt" body_template = get_email_template(email_template, "bugs") body = (body_template % body_data).strip() msg = bug_notification_builder.build( from_address, address, body, subject, email_date, rationale, references, msgid, filters=data["filter descriptions"], ) messages.append(msg) return filtered_notifications, omitted_notifications, messages
def construct_email_notifications(bug_notifications): """Construct an email from a list of related bug notifications. The person and bug has to be the same for all notifications, and there can be only one comment. """ first_notification = bug_notifications[0] bug = first_notification.bug actor = first_notification.message.owner subject = first_notification.message.subject comment = None references = [] text_notifications = [] old_values = {} new_values = {} for notification in bug_notifications: assert notification.bug == bug, bug.id assert notification.message.owner == actor, actor.id if notification.is_comment: assert comment is None, ( "Only one of the notifications is allowed to be a comment.") comment = notification.message else: key = get_activity_key(notification) if key is not None: if key not in old_values: old_values[key] = notification.activity.oldvalue new_values[key] = notification.activity.newvalue recipients = {} filtered_notifications = [] omitted_notifications = [] for notification in bug_notifications: key = get_activity_key(notification) if (notification.is_comment or key is None or key == 'removed_subscriber' or old_values[key] != new_values[key]): # We will report this notification. filtered_notifications.append(notification) for subscription_source in notification.recipients: for recipient in get_recipients(subscription_source.person): # The subscription_source.person may be a person or a # team. The get_recipients function gives us everyone # who should actually get an email for that person. # If subscription_source.person is a person or a team # with a preferred email address, then the people to # be emailed will only be subscription_source.person. # However, if it is a team without a preferred email # address, then this list will be the people and teams # that comprise the team, transitively, stopping the walk # at each person and at each team with a preferred email # address. sources_for_person = recipients.get(recipient) if sources_for_person is None: sources_for_person = [] recipients[recipient] = sources_for_person sources_for_person.append(subscription_source) else: omitted_notifications.append(notification) # If the actor does not want self-generated bug notifications, remove the # actor now. if not actor.selfgenerated_bugnotifications: recipients.pop(actor, None) if bug.duplicateof is not None: text_notifications.append( '*** This bug is a duplicate of bug %d ***\n %s' % (bug.duplicateof.id, canonical_url(bug.duplicateof))) if comment is not None: if comment == bug.initial_message: subject, text = generate_bug_add_email(bug) else: text = comment.text_contents text_notifications.append(text) msgid = comment.rfc822msgid email_date = comment.datecreated reference = comment.parent while reference is not None: references.insert(0, reference.rfc822msgid) reference = reference.parent else: msgid = first_notification.message.rfc822msgid email_date = first_notification.message.datecreated for notification in filtered_notifications: if notification.message == comment: # Comments were just handled in the previous if block. continue text = notification.message.text_contents.rstrip() text_notifications.append(text) if bug.initial_message.rfc822msgid not in references: # Ensure that references contain the initial message ID references.insert(0, bug.initial_message.rfc822msgid) # At this point we've got the data we need to construct the # messages. Now go ahead and actually do that. messages = [] mail_wrapper = MailWrapper(width=72) content = '\n\n'.join(text_notifications) from_address = get_bugmail_from_address(actor, bug) bug_notification_builder = BugNotificationBuilder(bug, actor) recipients = getUtility(IBugNotificationSet).getRecipientFilterData( bug, recipients, filtered_notifications) sorted_recipients = sorted(recipients.items(), key=lambda t: t[0].preferredemail.email) for email_person, data in sorted_recipients: address = str(email_person.preferredemail.email) # Choosing the first source is a bit arbitrary, but it # is simple for the user to understand. We may want to reconsider # this in the future. reason = data['sources'][0].reason_body rationale = data['sources'][0].reason_header if data['filter descriptions']: # There are some filter descriptions as well. Add them to # the email body. filters_text = u"\nMatching subscriptions: %s" % ", ".join( data['filter descriptions']) else: filters_text = u"" # In the rare case of a bug with no bugtasks, we can't generate the # subscription management URL so just leave off the subscription # management message entirely. if len(bug.bugtasks): bug_url = canonical_url(bug.bugtasks[0]) notification_url = bug_url + '/+subscriptions' subscriptions_message = ( "To manage notifications about this bug go to:\n%s" % notification_url) else: subscriptions_message = '' data_wrapper = MailWrapper(width=72, indent=' ') body_data = { 'content': mail_wrapper.format(content), 'bug_title': data_wrapper.format(bug.title), 'bug_url': canonical_url(bug), 'notification_rationale': mail_wrapper.format(reason), 'subscription_filters': filters_text, 'subscriptions_message': subscriptions_message, } # If the person we're sending to receives verbose notifications # we include the description and status of the bug in the email # footer. if email_person.verbose_bugnotifications: email_template = 'bug-notification-verbose.txt' body_data['bug_description'] = data_wrapper.format(bug.description) status_base = "Status in %s:\n %s" status_strings = [] for bug_task in bug.bugtasks: status_strings.append( status_base % (bug_task.target.title, bug_task.status.title)) body_data['bug_statuses'] = "\n".join(status_strings) else: email_template = 'bug-notification.txt' body_template = get_email_template(email_template, 'bugs') body = (body_template % body_data).strip() msg = bug_notification_builder.build( from_address, address, body, subject, email_date, rationale, references, msgid, filters=data['filter descriptions']) messages.append(msg) return filtered_notifications, omitted_notifications, messages