Merge lp:~stevenk/launchpad/announcements-copies into lp:launchpad

Proposed by Steve Kowalik
Status: Merged
Approved by: Steve Kowalik
Approved revision: no longer in the source branch.
Merged at revision: 13085
Proposed branch: lp:~stevenk/launchpad/announcements-copies
Merge into: lp:launchpad
Diff against target: 1327 lines (+653/-590)
2 files modified
lib/lp/soyuz/adapters/notification.py (+648/-0)
lib/lp/soyuz/model/queue.py (+5/-590)
To merge this branch: bzr merge lp:~stevenk/launchpad/announcements-copies
Reviewer Review Type Date Requested Status
Gavin Panella (community) Approve
Review via email: mp+61516@code.launchpad.net

Commit message

[r=allenap][no-qa] Move PackageUpload e-mail notifications to lp.soyuz.adapters.notification.

Description of the change

As the first step of generalising notifications so that copies can also perform them, move all of the e-mail type stuff out of lp.soyuz.model.queue and into lp.soyuz.adapters.notification.

This does not change any tests or functionality, it moves the code and massages it to work since they are no longer methods on a PackageUpload class.

I also drive-by added the Copyright header back to lp.soyuz.model.queue which Julian accidentally removed over 2,500 revisions ago.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote :

Looks fine.

I wonder if it might have been worth making these functions as methods
on a new notification class, so that state like dry_run doesn't need
to be passed around all the time, so that the logging methods can be
wrapped, etc. I guess that's work for a follow-up.

[1]

+def notification(blamer, changesfile, archive, distroseries, pocket, action,
+ actor=None, reason=None):
+ pass
+
+
+def notify_spr_less(blamer, upload_path, changesfiles, reason):
+ pass

These aren't used, and aren't particularly useful. Ditch them?

[2]

+ if dry_run and packageupload.logger is not None:
+ packageupload.logger.info("Would have sent a mail:")
[...]
+ else:
[...]
+ # And finally send the message.
+ sendmail(message)

This bug was present before; if dry_run is True but the logger is
None, the message will be sent. That doesn't seem quite right?

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/soyuz/adapters/notification.py'
2--- lib/lp/soyuz/adapters/notification.py 1970-01-01 00:00:00 +0000
3+++ lib/lp/soyuz/adapters/notification.py 2011-05-19 09:14:26 +0000
4@@ -0,0 +1,648 @@
5+# Copyright 2011 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+"""Notification for uploads and copies."""
9+
10+__metaclass__ = type
11+
12+__all__ = [
13+ 'notify',
14+ ]
15+
16+
17+from email.mime.multipart import MIMEMultipart
18+from email.mime.text import MIMEText
19+
20+from canonical.config import config
21+from canonical.launchpad.helpers import get_email_template
22+from canonical.launchpad.mail import (
23+ format_address,
24+ sendmail,
25+ )
26+from canonical.launchpad.webapp import canonical_url
27+from lp.archivepublisher.utils import get_ppa_reference
28+from lp.archiveuploader.changesfile import ChangesFile
29+from lp.registry.interfaces.pocket import (
30+ PackagePublishingPocket,
31+ pocketsuffix,
32+ )
33+from lp.services.encoding import (
34+ ascii_smash,
35+ guess as guess_encoding,
36+ )
37+from lp.soyuz.enums import PackageUploadStatus
38+
39+
40+def notification(blamer, changesfile, archive, distroseries, pocket, action,
41+ actor=None, reason=None):
42+ pass
43+
44+
45+def notify_spr_less(blamer, upload_path, changesfiles, reason):
46+ pass
47+
48+
49+def notify(packageupload, announce_list=None, summary_text=None,
50+ changes_file_object=None, logger=None, dry_run=False,
51+ allow_unsigned=None):
52+ """See `IPackageUpload`."""
53+
54+ packageupload.logger = logger
55+
56+ # If this is a binary or mixed upload, we don't send *any* emails
57+ # provided it's not a rejection or a security upload:
58+ if(packageupload.from_build and
59+ packageupload.status != PackageUploadStatus.REJECTED and
60+ packageupload.pocket != PackagePublishingPocket.SECURITY):
61+ debug(
62+ packageupload.logger,
63+ "Not sending email; upload is from a build.")
64+ return
65+
66+ # XXX julian 2007-05-11:
67+ # Requiring an open changesfile object is a bit ugly but it is
68+ # required because of several problems:
69+ # a) We don't know if the librarian has the file committed or not yet
70+ # b) Passing a ChangesFile object instead means that we get an
71+ # unordered dictionary which can't be translated back exactly for
72+ # the email's summary section.
73+ # For now, it's just easier to re-read the original file if the caller
74+ # requires us to do that instead of using the librarian's copy.
75+ changes, changes_lines = packageupload._getChangesDict(
76+ changes_file_object, allow_unsigned=allow_unsigned)
77+
78+ # "files" will contain a list of tuples of filename,component,section.
79+ # If files is empty, we don't need to send an email if this is not
80+ # a rejection.
81+ try:
82+ files = _buildUploadedFilesList(packageupload)
83+ except LanguagePackEncountered:
84+ # Don't send emails for language packs.
85+ return
86+
87+ if not files and packageupload.status != PackageUploadStatus.REJECTED:
88+ return
89+
90+ summary = _buildSummary(packageupload, files)
91+ if summary_text:
92+ summary.append(summary_text)
93+ summarystring = "\n".join(summary)
94+
95+ recipients = _getRecipients(packageupload, changes)
96+
97+ # There can be no recipients if none of the emails are registered
98+ # in LP.
99+ if not recipients:
100+ debug(packageupload.logger, "No recipients on email, not sending.")
101+ return
102+
103+ # Make the content of the actual changes file available to the
104+ # various email generating/sending functions.
105+ if changes_file_object is not None:
106+ changesfile_content = changes_file_object.read()
107+ else:
108+ changesfile_content = 'No changes file content available'
109+
110+ # If we need to send a rejection, do it now and return early.
111+ if packageupload.status == PackageUploadStatus.REJECTED:
112+ _sendRejectionNotification(
113+ packageupload, recipients, changes_lines, changes, summary_text,
114+ dry_run, changesfile_content)
115+ return
116+
117+ _sendSuccessNotification(
118+ packageupload, recipients, announce_list, changes_lines, changes,
119+ summarystring, dry_run, changesfile_content)
120+
121+
122+def _sendSuccessNotification(
123+ packageupload, recipients, announce_list, changes_lines, changes,
124+ summarystring, dry_run, changesfile_content):
125+ """Send a success email."""
126+
127+ def do_sendmail(message, recipients=recipients, from_addr=None,
128+ bcc=None):
129+ """Perform substitutions on a template and send the email."""
130+ _handleCommonBodyContent(packageupload, message, changes)
131+ body = message.template % message.__dict__
132+
133+ # Weed out duplicate name entries.
134+ names = ', '.join(set(packageupload.displayname.split(', ')))
135+
136+ # Construct the suite name according to Launchpad/Soyuz
137+ # convention.
138+ pocket_suffix = pocketsuffix[packageupload.pocket]
139+ if pocket_suffix:
140+ suite = '%s%s' % (packageupload.distroseries.name, pocket_suffix)
141+ else:
142+ suite = packageupload.distroseries.name
143+
144+ subject = '[%s/%s] %s %s (%s)' % (
145+ packageupload.distroseries.distribution.name, suite, names,
146+ packageupload.displayversion, message.STATUS)
147+
148+ if packageupload.isPPA():
149+ subject = "[PPA %s] %s" % (
150+ get_ppa_reference(packageupload.archive), subject)
151+ attach_changes = False
152+ else:
153+ attach_changes = True
154+
155+ _sendMail(
156+ packageupload, recipients, subject, body, dry_run,
157+ from_addr=from_addr, bcc=bcc,
158+ changesfile_content=changesfile_content,
159+ attach_changes=attach_changes)
160+
161+ class NewMessage:
162+ """New message."""
163+ template = get_email_template('upload-new.txt')
164+
165+ STATUS = "New"
166+ SUMMARY = summarystring
167+ CHANGESFILE = sanitize_string(
168+ ChangesFile.formatChangesComment(changes['Changes']))
169+ DISTRO = packageupload.distroseries.distribution.title
170+ if announce_list:
171+ ANNOUNCE = 'Announcing to %s' % announce_list
172+ else:
173+ ANNOUNCE = 'No announcement sent'
174+
175+ class UnapprovedMessage:
176+ """Unapproved message."""
177+ template = get_email_template('upload-accepted.txt')
178+
179+ STATUS = "Waiting for approval"
180+ SUMMARY = summarystring + (
181+ "\nThis upload awaits approval by a distro manager\n")
182+ CHANGESFILE = sanitize_string(
183+ ChangesFile.formatChangesComment(changes['Changes']))
184+ DISTRO = packageupload.distroseries.distribution.title
185+ if announce_list:
186+ ANNOUNCE = 'Announcing to %s' % announce_list
187+ else:
188+ ANNOUNCE = 'No announcement sent'
189+ CHANGEDBY = ''
190+ ORIGIN = ''
191+ SIGNER = ''
192+ MAINTAINER = ''
193+ SPR_URL = ''
194+
195+ class AcceptedMessage:
196+ """Accepted message."""
197+ template = get_email_template('upload-accepted.txt')
198+
199+ STATUS = "Accepted"
200+ SUMMARY = summarystring
201+ CHANGESFILE = sanitize_string(
202+ ChangesFile.formatChangesComment(changes['Changes']))
203+ DISTRO = packageupload.distroseries.distribution.title
204+ if announce_list:
205+ ANNOUNCE = 'Announcing to %s' % announce_list
206+ else:
207+ ANNOUNCE = 'No announcement sent'
208+ CHANGEDBY = ''
209+ ORIGIN = ''
210+ SIGNER = ''
211+ MAINTAINER = ''
212+ SPR_URL = ''
213+
214+ class PPAAcceptedMessage:
215+ """PPA accepted message."""
216+ template = get_email_template('ppa-upload-accepted.txt')
217+
218+ STATUS = "Accepted"
219+ SUMMARY = summarystring
220+ CHANGESFILE = guess_encoding(
221+ ChangesFile.formatChangesComment("".join(changes_lines)))
222+
223+ class AnnouncementMessage:
224+ template = get_email_template('upload-announcement.txt')
225+
226+ STATUS = "Accepted"
227+ SUMMARY = summarystring
228+ CHANGESFILE = sanitize_string(
229+ ChangesFile.formatChangesComment(changes['Changes']))
230+ CHANGEDBY = ''
231+ ORIGIN = ''
232+ SIGNER = ''
233+ MAINTAINER = ''
234+ SPR_URL = ''
235+
236+ # The template is ready. The remainder of this function deals with
237+ # whether to send a 'new' message, an acceptance message and/or an
238+ # announcement message.
239+
240+ if packageupload.status == PackageUploadStatus.NEW:
241+ # This is an unknown upload.
242+ do_sendmail(NewMessage)
243+ return
244+
245+ # Unapproved uploads coming from an insecure policy only send
246+ # an acceptance message.
247+ if packageupload.status == PackageUploadStatus.UNAPPROVED:
248+ # Only send an acceptance message.
249+ do_sendmail(UnapprovedMessage)
250+ return
251+
252+ if packageupload.isPPA():
253+ # PPA uploads receive an acceptance message.
254+ do_sendmail(PPAAcceptedMessage)
255+ return
256+
257+ # Auto-approved uploads to backports skips the announcement,
258+ # they are usually processed with the sync policy.
259+ if packageupload.pocket == PackagePublishingPocket.BACKPORTS:
260+ debug(
261+ packageupload.logger, "Skipping announcement, it is a BACKPORT.")
262+
263+ do_sendmail(AcceptedMessage)
264+ return
265+
266+ # Auto-approved binary-only uploads to security skip the
267+ # announcement, they are usually processed with the security policy.
268+ if (packageupload.pocket == PackagePublishingPocket.SECURITY
269+ and not packageupload.contains_source):
270+ # We only send announcements if there is any source in the upload.
271+ debug(packageupload.logger,
272+ "Skipping announcement, it is a binary upload to SECURITY.")
273+ do_sendmail(AcceptedMessage)
274+ return
275+
276+ # Fallback, all the rest coming from insecure, secure and sync
277+ # policies should send an acceptance and an announcement message.
278+ do_sendmail(AcceptedMessage)
279+
280+ # Don't send announcements for Debian auto sync uploads.
281+ if packageupload.isAutoSyncUpload(changed_by_email=changes['Changed-By']):
282+ return
283+
284+ if announce_list:
285+ if not packageupload.signing_key:
286+ from_addr = None
287+ else:
288+ from_addr = guess_encoding(changes['Changed-By'])
289+
290+ do_sendmail(
291+ AnnouncementMessage,
292+ recipients=[str(announce_list)],
293+ from_addr=from_addr,
294+ bcc="%s_derivatives@packages.qa.debian.org" %
295+ packageupload.displayname)
296+
297+
298+def _sendRejectionNotification(
299+ packageupload, recipients, changes_lines, changes, summary_text, dry_run,
300+ changesfile_content):
301+ """Send a rejection email."""
302+
303+ class PPARejectedMessage:
304+ """PPA rejected message."""
305+ template = get_email_template('ppa-upload-rejection.txt')
306+ SUMMARY = sanitize_string(summary_text)
307+ CHANGESFILE = sanitize_string(
308+ ChangesFile.formatChangesComment("".join(changes_lines)))
309+ USERS_ADDRESS = config.launchpad.users_address
310+
311+ class RejectedMessage:
312+ """Rejected message."""
313+ template = get_email_template('upload-rejection.txt')
314+ SUMMARY = sanitize_string(summary_text)
315+ CHANGESFILE = sanitize_string(
316+ ChangesFile.formatChangesComment(changes['Changes']))
317+ CHANGEDBY = ''
318+ ORIGIN = ''
319+ SIGNER = ''
320+ MAINTAINER = ''
321+ SPR_URL = ''
322+ USERS_ADDRESS = config.launchpad.users_address,
323+
324+ default_recipient = "%s <%s>" % (
325+ config.uploader.default_recipient_name,
326+ config.uploader.default_recipient_address)
327+ if not recipients:
328+ recipients = [default_recipient]
329+
330+ debug(packageupload.logger, "Sending rejection email.")
331+ if packageupload.isPPA():
332+ message = PPARejectedMessage
333+ attach_changes = False
334+ else:
335+ message = RejectedMessage
336+ attach_changes = True
337+
338+ _handleCommonBodyContent(packageupload, message, changes)
339+ if summary_text is None:
340+ message.SUMMARY = 'Rejected by archive administrator.'
341+
342+ body = message.template % message.__dict__
343+
344+ subject = "%s rejected" % packageupload.changesfile.filename
345+ if packageupload.isPPA():
346+ subject = "[PPA %s] %s" % (
347+ get_ppa_reference(packageupload.archive), subject)
348+
349+ _sendMail(
350+ packageupload, recipients, subject, body, dry_run,
351+ changesfile_content=changesfile_content,
352+ attach_changes=attach_changes)
353+
354+
355+def _sendMail(
356+ packageupload, to_addrs, subject, mail_text, dry_run, from_addr=None,
357+ bcc=None, changesfile_content=None, attach_changes=False):
358+ """Send an email to to_addrs with the given text and subject.
359+
360+ :to_addrs: A list of email addresses to be used as recipients. Each
361+ email must be a valid ASCII str instance or a unicode one.
362+ :subject: The email's subject.
363+ :mail_text: The text body of the email. Unicode is preserved in the
364+ email.
365+ :dry_run: Whether or not an email should actually be sent. But
366+ please note that this flag is (largely) ignored.
367+ :from_addr: The email address to be used as the sender. Must be a
368+ valid ASCII str instance or a unicode one. Defaults to the email
369+ for config.uploader.
370+ :bcc: Optional email Blind Carbon Copy address(es).
371+ :changesfile_content: The content of the actual changesfile.
372+ :attach_changes: A flag governing whether the original changesfile
373+ content shall be attached to the email.
374+ """
375+ extra_headers = {'X-Katie': 'Launchpad actually'}
376+
377+ # XXX cprov 20071212: ideally we only need to check archive.purpose,
378+ # however the current code in uploadprocessor.py (around line 259)
379+ # temporarily transforms the primary-archive into a PPA one (w/o
380+ # setting a proper owner) in order to allow processing of a upload
381+ # to unknown PPA and subsequent rejection notification.
382+
383+ # Include the 'X-Launchpad-PPA' header for PPA upload notfications
384+ # containing the PPA owner name.
385+ if (
386+ packageupload.archive.is_ppa and
387+ packageupload.archive.owner is not None):
388+ extra_headers['X-Launchpad-PPA'] = get_ppa_reference(
389+ packageupload.archive)
390+
391+ # Include a 'X-Launchpad-Component' header with the component and
392+ # the section of the source package uploaded in order to facilitate
393+ # filtering on the part of the email recipients.
394+ if packageupload.sources:
395+ spr = packageupload.my_source_package_release
396+ xlp_component_header = 'component=%s, section=%s' % (
397+ spr.component.name, spr.section.name)
398+ extra_headers['X-Launchpad-Component'] = xlp_component_header
399+
400+ if from_addr is None:
401+ from_addr = format_address(
402+ config.uploader.default_sender_name,
403+ config.uploader.default_sender_address)
404+
405+ # `sendmail`, despite handling unicode message bodies, can't
406+ # cope with non-ascii sender/recipient addresses, so ascii_smash
407+ # is used on all addresses.
408+
409+ # All emails from here have a Bcc to the default recipient.
410+ bcc_text = format_address(
411+ config.uploader.default_recipient_name,
412+ config.uploader.default_recipient_address)
413+ if bcc:
414+ bcc_text = "%s, %s" % (bcc_text, bcc)
415+ extra_headers['Bcc'] = ascii_smash(bcc_text)
416+
417+ recipients = ascii_smash(", ".join(to_addrs))
418+ if isinstance(from_addr, unicode):
419+ # ascii_smash only works on unicode strings.
420+ from_addr = ascii_smash(from_addr)
421+ else:
422+ from_addr.encode('ascii')
423+
424+ if dry_run and packageupload.logger is not None:
425+ packageupload.logger.info("Would have sent a mail:")
426+ packageupload.logger.info(" Subject: %s" % subject)
427+ packageupload.logger.info(" Sender: %s" % from_addr)
428+ packageupload.logger.info(" Recipients: %s" % recipients)
429+ packageupload.logger.info(" Bcc: %s" % extra_headers['Bcc'])
430+ packageupload.logger.info(" Body:")
431+ for line in mail_text.splitlines():
432+ packageupload.logger.info(line)
433+ else:
434+ debug(packageupload.logger, "Sent a mail:")
435+ debug(packageupload.logger, " Subject: %s" % subject)
436+ debug(packageupload.logger, " Recipients: %s" % recipients)
437+ debug(packageupload.logger, " Body:")
438+ for line in mail_text.splitlines():
439+ debug(packageupload.logger, line)
440+
441+ # Since we need to send the original changesfile as an
442+ # attachment the sendmail() method will be used as opposed to
443+ # simple_sendmail().
444+ message = MIMEMultipart()
445+ message['from'] = from_addr
446+ message['subject'] = subject
447+ message['to'] = recipients
448+
449+ # Set the extra headers if any are present.
450+ for key, value in extra_headers.iteritems():
451+ message.add_header(key, value)
452+
453+ # Add the email body.
454+ message.attach(MIMEText(
455+ sanitize_string(mail_text).encode('utf-8'), 'plain', 'utf-8'))
456+
457+ if attach_changes:
458+ # Add the original changesfile as an attachment.
459+ if changesfile_content is not None:
460+ changesfile_text = sanitize_string(changesfile_content)
461+ else:
462+ changesfile_text = ("Sorry, changesfile not available.")
463+
464+ attachment = MIMEText(
465+ changesfile_text.encode('utf-8'), 'plain', 'utf-8')
466+ attachment.add_header(
467+ 'Content-Disposition',
468+ 'attachment; filename="changesfile"')
469+ message.attach(attachment)
470+
471+ # And finally send the message.
472+ sendmail(message)
473+
474+
475+def _handleCommonBodyContent(packageupload, message, changes):
476+ """Put together pieces of the body common to all emails.
477+
478+ Sets the date, changed-by, maintainer, signer and origin properties on
479+ the message as appropriate.
480+
481+ :message: An object containing the various pieces of the notification
482+ email.
483+ :changes: A dictionary with the changes file content.
484+ """
485+ # Add the date field.
486+ message.DATE = 'Date: %s' % changes['Date']
487+
488+ # Add the debian 'Changed-By:' field.
489+ changed_by = changes.get('Changed-By')
490+ if changed_by is not None:
491+ changed_by = sanitize_string(changed_by)
492+ message.CHANGEDBY = '\nChanged-By: %s' % changed_by
493+
494+ # Add maintainer if present and different from changed-by.
495+ maintainer = changes.get('Maintainer')
496+ if maintainer is not None:
497+ maintainer = sanitize_string(maintainer)
498+ if maintainer != changed_by:
499+ message.MAINTAINER = '\nMaintainer: %s' % maintainer
500+
501+ # Add a 'Signed-By:' line if this is a signed upload and the
502+ # signer/sponsor differs from the changed-by.
503+ if packageupload.signing_key is not None:
504+ # This is a signed upload.
505+ signer = packageupload.signing_key.owner
506+
507+ signer_name = sanitize_string(signer.displayname)
508+ signer_email = sanitize_string(signer.preferredemail.email)
509+
510+ signer_signature = '%s <%s>' % (signer_name, signer_email)
511+
512+ if changed_by != signer_signature:
513+ message.SIGNER = '\nSigned-By: %s' % signer_signature
514+
515+ # Add the debian 'Origin:' field if present.
516+ if changes.get('Origin') is not None:
517+ message.ORIGIN = '\nOrigin: %s' % changes['Origin']
518+
519+ if packageupload.sources or packageupload.builds:
520+ message.SPR_URL = canonical_url(
521+ packageupload.my_source_package_release)
522+
523+
524+def sanitize_string(s):
525+ """Make sure string does not trigger 'ascii' codec errors.
526+
527+ Convert string to unicode if needed so that characters outside
528+ the (7-bit) ASCII range do not cause errors like these:
529+
530+ 'ascii' codec can't decode byte 0xc4 in position 21: ordinal
531+ not in range(128)
532+ """
533+ if isinstance(s, unicode):
534+ return s
535+ else:
536+ return guess_encoding(s)
537+
538+
539+def debug(logger, msg):
540+ """Shorthand debug notation for publish() methods."""
541+ if logger is not None:
542+ logger.debug(msg)
543+
544+
545+def _getRecipients(packageupload, changes):
546+ """Return a list of recipients for notification emails."""
547+ candidate_recipients = []
548+ debug(packageupload.logger, "Building recipients list.")
549+ changer = packageupload._emailToPerson(changes['Changed-By'])
550+
551+ if packageupload.signing_key:
552+ # This is a signed upload.
553+ signer = packageupload.signing_key.owner
554+ candidate_recipients.append(signer)
555+ else:
556+ debug(packageupload.logger,
557+ "Changes file is unsigned, adding changer as recipient")
558+ candidate_recipients.append(changer)
559+
560+ if packageupload.isPPA():
561+ # For PPAs, any person or team mentioned explicitly in the
562+ # ArchivePermissions as uploaders for the archive will also
563+ # get emailed.
564+ uploaders = [
565+ permission.person for permission in
566+ packageupload.archive.getUploadersForComponent()]
567+ candidate_recipients.extend(uploaders)
568+
569+ # If this is not a PPA, we also consider maintainer and changed-by.
570+ if packageupload.signing_key and not packageupload.isPPA():
571+ maintainer = packageupload._emailToPerson(changes['Maintainer'])
572+ if (maintainer and maintainer != signer and
573+ maintainer.isUploader(
574+ packageupload.distroseries.distribution)):
575+ debug(packageupload.logger, "Adding maintainer to recipients")
576+ candidate_recipients.append(maintainer)
577+
578+ if (changer and changer != signer and
579+ changer.isUploader(packageupload.distroseries.distribution)):
580+ debug(packageupload.logger, "Adding changed-by to recipients")
581+ candidate_recipients.append(changer)
582+
583+ # Now filter list of recipients for persons only registered in
584+ # Launchpad to avoid spamming the innocent.
585+ recipients = []
586+ for person in candidate_recipients:
587+ if person is None or person.preferredemail is None:
588+ continue
589+ recipient = format_address(person.displayname,
590+ person.preferredemail.email)
591+ debug(packageupload.logger, "Adding recipient: '%s'" % recipient)
592+ recipients.append(recipient)
593+
594+ return recipients
595+
596+
597+def _buildUploadedFilesList(packageupload):
598+ """Return a list of tuples of (filename, component, section).
599+
600+ Component and section are only set where the file is a source upload.
601+ If an empty list is returned, it means there are no files.
602+ Raises LanguagePackRejection if a language pack is detected.
603+ No emails should be sent for language packs.
604+ """
605+ files = []
606+ if packageupload.contains_source:
607+ [source] = packageupload.sources
608+ spr = source.sourcepackagerelease
609+ # Bail out early if this is an upload for the translations
610+ # section.
611+ if spr.section.name == 'translations':
612+ debug(packageupload.logger,
613+ "Skipping acceptance and announcement, it is a "
614+ "language-package upload.")
615+ raise LanguagePackEncountered
616+ for sprfile in spr.files:
617+ files.append(
618+ (sprfile.libraryfile.filename, spr.component.name,
619+ spr.section.name))
620+
621+ # Component and section don't get set for builds and custom, since
622+ # this information is only used in the summary string for source
623+ # uploads.
624+ for build in packageupload.builds:
625+ for bpr in build.build.binarypackages:
626+ files.extend([
627+ (bpf.libraryfile.filename, '', '') for bpf in bpr.files])
628+
629+ if packageupload.customfiles:
630+ files.extend(
631+ [(file.libraryfilealias.filename, '', '')
632+ for file in packageupload.customfiles])
633+
634+ return files
635+
636+
637+def _buildSummary(packageupload, files):
638+ """Build a summary string based on the files present in the upload."""
639+ summary = []
640+ for filename, component, section in files:
641+ if packageupload.status == PackageUploadStatus.NEW:
642+ summary.append("NEW: %s" % filename)
643+ else:
644+ summary.append(" OK: %s" % filename)
645+ if filename.endswith("dsc"):
646+ summary.append(" -> Component: %s Section: %s" % (
647+ component, section))
648+ return summary
649+
650+
651+class LanguagePackEncountered(Exception):
652+ """Thrown when not wanting to email notifications for language packs."""
653
654=== modified file 'lib/lp/soyuz/model/queue.py'
655--- lib/lp/soyuz/model/queue.py 2011-03-22 14:27:50 +0000
656+++ lib/lp/soyuz/model/queue.py 2011-05-19 09:14:26 +0000
657@@ -1,3 +1,4 @@
658+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
659 # GNU Affero General Public License version 3 (see the file LICENSE).
660
661 # pylint: disable-msg=E0611,W0212
662@@ -12,8 +13,6 @@
663 'PackageUploadSet',
664 ]
665
666-from email.mime.multipart import MIMEMultipart
667-from email.mime.text import MIMEText
668 import os
669 import shutil
670 import StringIO
671@@ -40,19 +39,11 @@
672 SQLBase,
673 sqlvalues,
674 )
675-from lp.services.encoding import (
676- ascii_smash,
677- guess as guess_encoding,
678- )
679-from canonical.launchpad.helpers import get_email_template
680 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
681 from canonical.launchpad.interfaces.lpstorm import IMasterStore
682 from canonical.launchpad.mail import (
683- format_address,
684- sendmail,
685 signed_message_from_string,
686 )
687-from canonical.launchpad.webapp import canonical_url
688 from canonical.librarian.interfaces import DownloadFailed
689 from canonical.librarian.utils import copy_and_close
690 from lp.app.errors import NotFoundError
691@@ -61,8 +52,6 @@
692 # that it needs a bit of redesigning here around the publication stuff.
693 from lp.archivepublisher.config import getPubConfig
694 from lp.archivepublisher.customupload import CustomUploadError
695-from lp.archivepublisher.utils import get_ppa_reference
696-from lp.archiveuploader.changesfile import ChangesFile
697 from lp.archiveuploader.tagfiles import parse_tagfile_lines
698 from lp.archiveuploader.utils import safe_fix_maintainer
699 from lp.registry.interfaces.person import IPersonSet
700@@ -71,6 +60,7 @@
701 pocketsuffix,
702 )
703 from lp.services.propertycache import cachedproperty
704+from lp.soyuz.adapters.notification import notify
705 from lp.soyuz.enums import (
706 PackageUploadCustomFormat,
707 PackageUploadStatus,
708@@ -101,6 +91,7 @@
709 # of the archivepublisher which cause circular import errors if they
710 # are placed here.
711
712+
713 def debug(logger, msg):
714 """Shorthand debug notation for publish() methods."""
715 if logger is not None:
716@@ -127,21 +118,6 @@
717 'provided methods to set it.')
718
719
720-def sanitize_string(s):
721- """Make sure string does not trigger 'ascii' codec errors.
722-
723- Convert string to unicode if needed so that characters outside
724- the (7-bit) ASCII range do not cause errors like these:
725-
726- 'ascii' codec can't decode byte 0xc4 in position 21: ordinal
727- not in range(128)
728- """
729- if isinstance(s, unicode):
730- return s
731- else:
732- return guess_encoding(s)
733-
734-
735 class PackageUploadQueue:
736
737 implements(IPackageUploadQueue)
738@@ -151,10 +127,6 @@
739 self.status = status
740
741
742-class LanguagePackEncountered(Exception):
743- """Thrown when not wanting to email notifications for language packs."""
744-
745-
746 class PackageUpload(SQLBase):
747 """A Queue item for the archive uploader."""
748
749@@ -750,453 +722,12 @@
750
751 return changes, changes_lines
752
753- def _buildUploadedFilesList(self):
754- """Return a list of tuples of (filename, component, section).
755-
756- Component and section are only set where the file is a source upload.
757- If an empty list is returned, it means there are no files.
758- Raises LanguagePackRejection if a language pack is detected.
759- No emails should be sent for language packs.
760- """
761- files = []
762- if self.contains_source:
763- [source] = self.sources
764- spr = source.sourcepackagerelease
765- # Bail out early if this is an upload for the translations
766- # section.
767- if spr.section.name == 'translations':
768- debug(self.logger,
769- "Skipping acceptance and announcement, it is a "
770- "language-package upload.")
771- raise LanguagePackEncountered
772- for sprfile in spr.files:
773- files.append(
774- (sprfile.libraryfile.filename, spr.component.name,
775- spr.section.name))
776-
777- # Component and section don't get set for builds and custom, since
778- # this information is only used in the summary string for source
779- # uploads.
780- for build in self.builds:
781- for bpr in build.build.binarypackages:
782- files.extend([
783- (bpf.libraryfile.filename, '', '') for bpf in bpr.files])
784-
785- if self.customfiles:
786- files.extend(
787- [(file.libraryfilealias.filename, '', '')
788- for file in self.customfiles])
789-
790- return files
791-
792- def _buildSummary(self, files):
793- """Build a summary string based on the files present in the upload."""
794- summary = []
795- for filename, component, section in files:
796- if self.status == PackageUploadStatus.NEW:
797- summary.append("NEW: %s" % filename)
798- else:
799- summary.append(" OK: %s" % filename)
800- if filename.endswith("dsc"):
801- summary.append(" -> Component: %s Section: %s" % (
802- component, section))
803- return summary
804-
805- def _handleCommonBodyContent(self, message, changes):
806- """Put together pieces of the body common to all emails.
807-
808- Sets the date, changed-by, maintainer, signer and origin properties on
809- the message as appropriate.
810-
811- :message: An object containing the various pieces of the notification
812- email.
813- :changes: A dictionary with the changes file content.
814- """
815- # Add the date field.
816- message.DATE = 'Date: %s' % changes['Date']
817-
818- # Add the debian 'Changed-By:' field.
819- changed_by = changes.get('Changed-By')
820- if changed_by is not None:
821- changed_by = sanitize_string(changed_by)
822- message.CHANGEDBY = '\nChanged-By: %s' % changed_by
823-
824- # Add maintainer if present and different from changed-by.
825- maintainer = changes.get('Maintainer')
826- if maintainer is not None:
827- maintainer = sanitize_string(maintainer)
828- if maintainer != changed_by:
829- message.MAINTAINER = '\nMaintainer: %s' % maintainer
830-
831- # Add a 'Signed-By:' line if this is a signed upload and the
832- # signer/sponsor differs from the changed-by.
833- if self.signing_key is not None:
834- # This is a signed upload.
835- signer = self.signing_key.owner
836-
837- signer_name = sanitize_string(signer.displayname)
838- signer_email = sanitize_string(signer.preferredemail.email)
839-
840- signer_signature = '%s <%s>' % (signer_name, signer_email)
841-
842- if changed_by != signer_signature:
843- message.SIGNER = '\nSigned-By: %s' % signer_signature
844-
845- # Add the debian 'Origin:' field if present.
846- if changes.get('Origin') is not None:
847- message.ORIGIN = '\nOrigin: %s' % changes['Origin']
848-
849- if self.sources or self.builds:
850- message.SPR_URL = canonical_url(self.my_source_package_release)
851-
852- def _sendRejectionNotification(
853- self, recipients, changes_lines, changes, summary_text, dry_run,
854- changesfile_content):
855- """Send a rejection email."""
856-
857- class PPARejectedMessage:
858- """PPA rejected message."""
859- template = get_email_template('ppa-upload-rejection.txt')
860- SUMMARY = sanitize_string(summary_text)
861- CHANGESFILE = sanitize_string(
862- ChangesFile.formatChangesComment("".join(changes_lines)))
863- USERS_ADDRESS = config.launchpad.users_address
864-
865- class RejectedMessage:
866- """Rejected message."""
867- template = get_email_template('upload-rejection.txt')
868- SUMMARY = sanitize_string(summary_text)
869- CHANGESFILE = sanitize_string(
870- ChangesFile.formatChangesComment(changes['Changes']))
871- CHANGEDBY = ''
872- ORIGIN = ''
873- SIGNER = ''
874- MAINTAINER = ''
875- SPR_URL = ''
876- USERS_ADDRESS = config.launchpad.users_address,
877-
878- default_recipient = "%s <%s>" % (
879- config.uploader.default_recipient_name,
880- config.uploader.default_recipient_address)
881- if not recipients:
882- recipients = [default_recipient]
883-
884- debug(self.logger, "Sending rejection email.")
885- if self.isPPA():
886- message = PPARejectedMessage
887- attach_changes = False
888- else:
889- message = RejectedMessage
890- attach_changes = True
891-
892- self._handleCommonBodyContent(message, changes)
893- if summary_text is None:
894- message.SUMMARY = 'Rejected by archive administrator.'
895-
896- body = message.template % message.__dict__
897-
898- subject = "%s rejected" % self.changesfile.filename
899- if self.isPPA():
900- subject = "[PPA %s] %s" % (
901- get_ppa_reference(self.archive), subject)
902-
903- self._sendMail(
904- recipients, subject, body, dry_run,
905- changesfile_content=changesfile_content,
906- attach_changes=attach_changes)
907-
908- def _sendSuccessNotification(
909- self, recipients, announce_list, changes_lines, changes,
910- summarystring, dry_run, changesfile_content):
911- """Send a success email."""
912-
913- def do_sendmail(message, recipients=recipients, from_addr=None,
914- bcc=None):
915- """Perform substitutions on a template and send the email."""
916- self._handleCommonBodyContent(message, changes)
917- body = message.template % message.__dict__
918-
919- # Weed out duplicate name entries.
920- names = ', '.join(set(self.displayname.split(', ')))
921-
922- # Construct the suite name according to Launchpad/Soyuz
923- # convention.
924- pocket_suffix = pocketsuffix[self.pocket]
925- if pocket_suffix:
926- suite = '%s%s' % (self.distroseries.name, pocket_suffix)
927- else:
928- suite = self.distroseries.name
929-
930- subject = '[%s/%s] %s %s (%s)' % (
931- self.distroseries.distribution.name, suite, names,
932- self.displayversion, message.STATUS)
933-
934- if self.isPPA():
935- subject = "[PPA %s] %s" % (
936- get_ppa_reference(self.archive), subject)
937- attach_changes = False
938- else:
939- attach_changes = True
940-
941- self._sendMail(
942- recipients, subject, body, dry_run, from_addr=from_addr,
943- bcc=bcc, changesfile_content=changesfile_content,
944- attach_changes=attach_changes)
945-
946- class NewMessage:
947- """New message."""
948- template = get_email_template('upload-new.txt')
949-
950- STATUS = "New"
951- SUMMARY = summarystring
952- CHANGESFILE = sanitize_string(
953- ChangesFile.formatChangesComment(changes['Changes']))
954- DISTRO = self.distroseries.distribution.title
955- if announce_list:
956- ANNOUNCE = 'Announcing to %s' % announce_list
957- else:
958- ANNOUNCE = 'No announcement sent'
959-
960- class UnapprovedMessage:
961- """Unapproved message."""
962- template = get_email_template('upload-accepted.txt')
963-
964- STATUS = "Waiting for approval"
965- SUMMARY = summarystring + (
966- "\nThis upload awaits approval by a distro manager\n")
967- CHANGESFILE = sanitize_string(
968- ChangesFile.formatChangesComment(changes['Changes']))
969- DISTRO = self.distroseries.distribution.title
970- if announce_list:
971- ANNOUNCE = 'Announcing to %s' % announce_list
972- else:
973- ANNOUNCE = 'No announcement sent'
974- CHANGEDBY = ''
975- ORIGIN = ''
976- SIGNER = ''
977- MAINTAINER = ''
978- SPR_URL = ''
979-
980- class AcceptedMessage:
981- """Accepted message."""
982- template = get_email_template('upload-accepted.txt')
983-
984- STATUS = "Accepted"
985- SUMMARY = summarystring
986- CHANGESFILE = sanitize_string(
987- ChangesFile.formatChangesComment(changes['Changes']))
988- DISTRO = self.distroseries.distribution.title
989- if announce_list:
990- ANNOUNCE = 'Announcing to %s' % announce_list
991- else:
992- ANNOUNCE = 'No announcement sent'
993- CHANGEDBY = ''
994- ORIGIN = ''
995- SIGNER = ''
996- MAINTAINER = ''
997- SPR_URL = ''
998-
999- class PPAAcceptedMessage:
1000- """PPA accepted message."""
1001- template = get_email_template('ppa-upload-accepted.txt')
1002-
1003- STATUS = "Accepted"
1004- SUMMARY = summarystring
1005- CHANGESFILE = guess_encoding(
1006- ChangesFile.formatChangesComment("".join(changes_lines)))
1007-
1008- class AnnouncementMessage:
1009- template = get_email_template('upload-announcement.txt')
1010-
1011- STATUS = "Accepted"
1012- SUMMARY = summarystring
1013- CHANGESFILE = sanitize_string(
1014- ChangesFile.formatChangesComment(changes['Changes']))
1015- CHANGEDBY = ''
1016- ORIGIN = ''
1017- SIGNER = ''
1018- MAINTAINER = ''
1019- SPR_URL = ''
1020-
1021- # The template is ready. The remainder of this function deals with
1022- # whether to send a 'new' message, an acceptance message and/or an
1023- # announcement message.
1024-
1025- if self.status == PackageUploadStatus.NEW:
1026- # This is an unknown upload.
1027- do_sendmail(NewMessage)
1028- return
1029-
1030- # Unapproved uploads coming from an insecure policy only send
1031- # an acceptance message.
1032- if self.status == PackageUploadStatus.UNAPPROVED:
1033- # Only send an acceptance message.
1034- do_sendmail(UnapprovedMessage)
1035- return
1036-
1037- if self.isPPA():
1038- # PPA uploads receive an acceptance message.
1039- do_sendmail(PPAAcceptedMessage)
1040- return
1041-
1042- # Auto-approved uploads to backports skips the announcement,
1043- # they are usually processed with the sync policy.
1044- if self.pocket == PackagePublishingPocket.BACKPORTS:
1045- debug(self.logger, "Skipping announcement, it is a BACKPORT.")
1046-
1047- do_sendmail(AcceptedMessage)
1048- return
1049-
1050- # Auto-approved binary-only uploads to security skip the
1051- # announcement, they are usually processed with the security policy.
1052- if (self.pocket == PackagePublishingPocket.SECURITY
1053- and not self.contains_source):
1054- # We only send announcements if there is any source in the upload.
1055- debug(self.logger,
1056- "Skipping announcement, it is a binary upload to SECURITY.")
1057- do_sendmail(AcceptedMessage)
1058- return
1059-
1060- # Fallback, all the rest coming from insecure, secure and sync
1061- # policies should send an acceptance and an announcement message.
1062- do_sendmail(AcceptedMessage)
1063-
1064- # Don't send announcements for Debian auto sync uploads.
1065- if self.isAutoSyncUpload(changed_by_email=changes['Changed-By']):
1066- return
1067-
1068- if announce_list:
1069- if not self.signing_key:
1070- from_addr = None
1071- else:
1072- from_addr = guess_encoding(changes['Changed-By'])
1073-
1074- do_sendmail(
1075- AnnouncementMessage,
1076- recipients=[str(announce_list)],
1077- from_addr=from_addr,
1078- bcc="%s_derivatives@packages.qa.debian.org" %
1079- self.displayname)
1080-
1081 def notify(self, announce_list=None, summary_text=None,
1082 changes_file_object=None, logger=None, dry_run=False,
1083 allow_unsigned=None):
1084 """See `IPackageUpload`."""
1085-
1086- self.logger = logger
1087-
1088- # If this is a binary or mixed upload, we don't send *any* emails
1089- # provided it's not a rejection or a security upload:
1090- if(self.from_build and
1091- self.status != PackageUploadStatus.REJECTED and
1092- self.pocket != PackagePublishingPocket.SECURITY):
1093- debug(self.logger, "Not sending email; upload is from a build.")
1094- return
1095-
1096- # XXX julian 2007-05-11:
1097- # Requiring an open changesfile object is a bit ugly but it is
1098- # required because of several problems:
1099- # a) We don't know if the librarian has the file committed or not yet
1100- # b) Passing a ChangesFile object instead means that we get an
1101- # unordered dictionary which can't be translated back exactly for
1102- # the email's summary section.
1103- # For now, it's just easier to re-read the original file if the caller
1104- # requires us to do that instead of using the librarian's copy.
1105- changes, changes_lines = self._getChangesDict(
1106- changes_file_object, allow_unsigned=allow_unsigned)
1107-
1108- # "files" will contain a list of tuples of filename,component,section.
1109- # If files is empty, we don't need to send an email if this is not
1110- # a rejection.
1111- try:
1112- files = self._buildUploadedFilesList()
1113- except LanguagePackEncountered:
1114- # Don't send emails for language packs.
1115- return
1116-
1117- if not files and self.status != PackageUploadStatus.REJECTED:
1118- return
1119-
1120- summary = self._buildSummary(files)
1121- if summary_text:
1122- summary.append(summary_text)
1123- summarystring = "\n".join(summary)
1124-
1125- recipients = self._getRecipients(changes)
1126-
1127- # There can be no recipients if none of the emails are registered
1128- # in LP.
1129- if not recipients:
1130- debug(self.logger, "No recipients on email, not sending.")
1131- return
1132-
1133- # Make the content of the actual changes file available to the
1134- # various email generating/sending functions.
1135- if changes_file_object is not None:
1136- changesfile_content = changes_file_object.read()
1137- else:
1138- changesfile_content = 'No changes file content available'
1139-
1140- # If we need to send a rejection, do it now and return early.
1141- if self.status == PackageUploadStatus.REJECTED:
1142- self._sendRejectionNotification(
1143- recipients, changes_lines, changes, summary_text, dry_run,
1144- changesfile_content)
1145- return
1146-
1147- self._sendSuccessNotification(
1148- recipients, announce_list, changes_lines, changes, summarystring,
1149- dry_run, changesfile_content)
1150-
1151- def _getRecipients(self, changes):
1152- """Return a list of recipients for notification emails."""
1153- candidate_recipients = []
1154- debug(self.logger, "Building recipients list.")
1155- changer = self._emailToPerson(changes['Changed-By'])
1156-
1157- if self.signing_key:
1158- # This is a signed upload.
1159- signer = self.signing_key.owner
1160- candidate_recipients.append(signer)
1161- else:
1162- debug(self.logger,
1163- "Changes file is unsigned, adding changer as recipient")
1164- candidate_recipients.append(changer)
1165-
1166- if self.isPPA():
1167- # For PPAs, any person or team mentioned explicitly in the
1168- # ArchivePermissions as uploaders for the archive will also
1169- # get emailed.
1170- uploaders = [
1171- permission.person for permission in
1172- self.archive.getUploadersForComponent()]
1173- candidate_recipients.extend(uploaders)
1174-
1175- # If this is not a PPA, we also consider maintainer and changed-by.
1176- if self.signing_key and not self.isPPA():
1177- maintainer = self._emailToPerson(changes['Maintainer'])
1178- if (maintainer and maintainer != signer and
1179- maintainer.isUploader(self.distroseries.distribution)):
1180- debug(self.logger, "Adding maintainer to recipients")
1181- candidate_recipients.append(maintainer)
1182-
1183- if (changer and changer != signer and
1184- changer.isUploader(self.distroseries.distribution)):
1185- debug(self.logger, "Adding changed-by to recipients")
1186- candidate_recipients.append(changer)
1187-
1188- # Now filter list of recipients for persons only registered in
1189- # Launchpad to avoid spamming the innocent.
1190- recipients = []
1191- for person in candidate_recipients:
1192- if person is None or person.preferredemail is None:
1193- continue
1194- recipient = format_address(person.displayname,
1195- person.preferredemail.email)
1196- debug(self.logger, "Adding recipient: '%s'" % recipient)
1197- recipients.append(recipient)
1198-
1199- return recipients
1200+ notify(self, announce_list, summary_text, changes_file_object,
1201+ logger, dry_run, allow_unsigned)
1202
1203 # XXX julian 2007-05-21:
1204 # This method should really be IPersonSet.getByUploader but requires
1205@@ -1219,122 +750,6 @@
1206 debug(self.logger, "Decision: %s" % uploader)
1207 return uploader
1208
1209- def _sendMail(
1210- self, to_addrs, subject, mail_text, dry_run, from_addr=None, bcc=None,
1211- changesfile_content=None, attach_changes=False):
1212- """Send an email to to_addrs with the given text and subject.
1213-
1214- :to_addrs: A list of email addresses to be used as recipients. Each
1215- email must be a valid ASCII str instance or a unicode one.
1216- :subject: The email's subject.
1217- :mail_text: The text body of the email. Unicode is preserved in the
1218- email.
1219- :dry_run: Whether or not an email should actually be sent. But
1220- please note that this flag is (largely) ignored.
1221- :from_addr: The email address to be used as the sender. Must be a
1222- valid ASCII str instance or a unicode one. Defaults to the email
1223- for config.uploader.
1224- :bcc: Optional email Blind Carbon Copy address(es).
1225- :changesfile_content: The content of the actual changesfile.
1226- :attach_changes: A flag governing whether the original changesfile
1227- content shall be attached to the email.
1228- """
1229- extra_headers = {'X-Katie': 'Launchpad actually'}
1230-
1231- # XXX cprov 20071212: ideally we only need to check archive.purpose,
1232- # however the current code in uploadprocessor.py (around line 259)
1233- # temporarily transforms the primary-archive into a PPA one (w/o
1234- # setting a proper owner) in order to allow processing of a upload
1235- # to unknown PPA and subsequent rejection notification.
1236-
1237- # Include the 'X-Launchpad-PPA' header for PPA upload notfications
1238- # containing the PPA owner name.
1239- if (self.archive.is_ppa and self.archive.owner is not None):
1240- extra_headers['X-Launchpad-PPA'] = get_ppa_reference(self.archive)
1241-
1242- # Include a 'X-Launchpad-Component' header with the component and
1243- # the section of the source package uploaded in order to facilitate
1244- # filtering on the part of the email recipients.
1245- if self.sources:
1246- spr = self.my_source_package_release
1247- xlp_component_header = 'component=%s, section=%s' % (
1248- spr.component.name, spr.section.name)
1249- extra_headers['X-Launchpad-Component'] = xlp_component_header
1250-
1251- if from_addr is None:
1252- from_addr = format_address(
1253- config.uploader.default_sender_name,
1254- config.uploader.default_sender_address)
1255-
1256- # `sendmail`, despite handling unicode message bodies, can't
1257- # cope with non-ascii sender/recipient addresses, so ascii_smash
1258- # is used on all addresses.
1259-
1260- # All emails from here have a Bcc to the default recipient.
1261- bcc_text = format_address(
1262- config.uploader.default_recipient_name,
1263- config.uploader.default_recipient_address)
1264- if bcc:
1265- bcc_text = "%s, %s" % (bcc_text, bcc)
1266- extra_headers['Bcc'] = ascii_smash(bcc_text)
1267-
1268- recipients = ascii_smash(", ".join(to_addrs))
1269- if isinstance(from_addr, unicode):
1270- # ascii_smash only works on unicode strings.
1271- from_addr = ascii_smash(from_addr)
1272- else:
1273- from_addr.encode('ascii')
1274-
1275- if dry_run and self.logger is not None:
1276- self.logger.info("Would have sent a mail:")
1277- self.logger.info(" Subject: %s" % subject)
1278- self.logger.info(" Sender: %s" % from_addr)
1279- self.logger.info(" Recipients: %s" % recipients)
1280- self.logger.info(" Bcc: %s" % extra_headers['Bcc'])
1281- self.logger.info(" Body:")
1282- for line in mail_text.splitlines():
1283- self.logger.info(line)
1284- else:
1285- debug(self.logger, "Sent a mail:")
1286- debug(self.logger, " Subject: %s" % subject)
1287- debug(self.logger, " Recipients: %s" % recipients)
1288- debug(self.logger, " Body:")
1289- for line in mail_text.splitlines():
1290- debug(self.logger, line)
1291-
1292- # Since we need to send the original changesfile as an
1293- # attachment the sendmail() method will be used as opposed to
1294- # simple_sendmail().
1295- message = MIMEMultipart()
1296- message['from'] = from_addr
1297- message['subject'] = subject
1298- message['to'] = recipients
1299-
1300- # Set the extra headers if any are present.
1301- for key, value in extra_headers.iteritems():
1302- message.add_header(key, value)
1303-
1304- # Add the email body.
1305- message.attach(MIMEText(
1306- sanitize_string(mail_text).encode('utf-8'), 'plain', 'utf-8'))
1307-
1308- if attach_changes:
1309- # Add the original changesfile as an attachment.
1310- if changesfile_content is not None:
1311- changesfile_text = sanitize_string(changesfile_content)
1312- else:
1313- changesfile_text = ("Sorry, changesfile not available.")
1314-
1315- attachment = MIMEText(
1316- changesfile_text.encode('utf-8'), 'plain', 'utf-8')
1317- attachment.add_header(
1318- 'Content-Disposition',
1319- 'attachment; filename="changesfile"')
1320- message.attach(attachment)
1321-
1322- # And finally send the message.
1323- sendmail(message)
1324-
1325 @property
1326 def components(self):
1327 """See `IPackageUpload`."""