Merge lp:~stevenk/launchpad/announcements-copies into lp:launchpad
- announcements-copies
- Merge into devel
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 |
Related bugs: |
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.
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.
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.
To post a comment you must log in.
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`.""" |
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, spr_less( blamer, upload_path, changesfiles, reason):
+ actor=None, reason=None):
+ pass
+
+
+def notify_
+ pass
These aren't used, and aren't particularly useful. Ditch them?
[2]
+ if dry_run and packageupload. logger is not None: logger. info("Would have sent a mail:")
+ packageupload.
[...]
+ 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?