Package buildbot :: Package status :: Module mail
[frames] | no frames]

Source Code for Module buildbot.status.mail

  1  # This file is part of Buildbot.  Buildbot is free software: you can 
  2  # redistribute it and/or modify it under the terms of the GNU General Public 
  3  # License as published by the Free Software Foundation, version 2. 
  4  # 
  5  # This program is distributed in the hope that it will be useful, but WITHOUT 
  6  # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 
  7  # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more 
  8  # details. 
  9  # 
 10  # You should have received a copy of the GNU General Public License along with 
 11  # this program; if not, write to the Free Software Foundation, Inc., 51 
 12  # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 
 13  # 
 14  # Copyright Buildbot Team Members 
 15   
 16   
 17  import re 
 18  import types 
 19  from email.Message import Message 
 20  from email.Utils import formatdate 
 21  from email.MIMEText import MIMEText 
 22  from email.MIMENonMultipart import MIMENonMultipart 
 23  from email.MIMEMultipart import MIMEMultipart 
 24  from StringIO import StringIO 
 25  import urllib 
 26   
 27  from zope.interface import implements 
 28  from twisted.internet import defer, reactor 
 29  from twisted.python import log as twlog 
 30   
 31  try: 
 32      from twisted.mail.smtp import ESMTPSenderFactory 
 33      ESMTPSenderFactory = ESMTPSenderFactory # for pyflakes 
 34  except ImportError: 
 35      ESMTPSenderFactory = None 
 36   
 37  have_ssl = True 
 38  try: 
 39      from twisted.internet import ssl 
 40      from OpenSSL.SSL import SSLv3_METHOD 
 41  except ImportError: 
 42      have_ssl = False 
 43   
 44  # this incantation teaches email to output utf-8 using 7- or 8-bit encoding, 
 45  # although it has no effect before python-2.7. 
 46  from email import Charset 
 47  Charset.add_charset('utf-8', Charset.SHORTEST, None, 'utf-8') 
 48   
 49  from buildbot import interfaces, util, config 
 50  from buildbot.process.users import users 
 51  from buildbot.status import base 
 52  from buildbot.status.results import FAILURE, SUCCESS, WARNINGS, EXCEPTION, Results 
 53   
 54  VALID_EMAIL = re.compile("[a-zA-Z0-9\.\_\%\-\+]+@[a-zA-Z0-9\.\_\%\-]+.[a-zA-Z]{2,6}") 
 55   
 56  ENCODING = 'utf8' 
 57  LOG_ENCODING = 'utf-8' 
58 59 -class Domain(util.ComparableMixin):
60 implements(interfaces.IEmailLookup) 61 compare_attrs = ["domain"] 62
63 - def __init__(self, domain):
64 assert "@" not in domain 65 self.domain = domain
66
67 - def getAddress(self, name):
68 """If name is already an email address, pass it through.""" 69 if '@' in name: 70 return name 71 return name + "@" + self.domain
72
73 74 -def defaultMessage(mode, name, build, results, master_status):
75 """Generate a buildbot mail message and return a tuple of message text 76 and type.""" 77 ss_list = build.getSourceStamps() 78 79 prev = build.getPreviousBuild() 80 81 text = "" 82 if results == FAILURE: 83 if "change" in mode and prev and prev.getResults() != results or \ 84 "problem" in mode and prev and prev.getResults() != FAILURE: 85 text += "The Buildbot has detected a new failure" 86 else: 87 text += "The Buildbot has detected a failed build" 88 elif results == WARNINGS: 89 text += "The Buildbot has detected a problem in the build" 90 elif results == SUCCESS: 91 if "change" in mode and prev and prev.getResults() != results: 92 text += "The Buildbot has detected a restored build" 93 else: 94 text += "The Buildbot has detected a passing build" 95 elif results == EXCEPTION: 96 text += "The Buildbot has detected a build exception" 97 98 projects = [] 99 if ss_list: 100 for ss in ss_list: 101 if ss.project and ss.project not in projects: 102 projects.append(ss.project) 103 if not projects: 104 projects = [master_status.getTitle()] 105 text += " on builder %s while building %s.\n" % (name, ', '.join(projects)) 106 107 if master_status.getURLForThing(build): 108 text += "Full details are available at:\n %s\n" % master_status.getURLForThing(build) 109 text += "\n" 110 111 if master_status.getBuildbotURL(): 112 text += "Buildbot URL: %s\n\n" % urllib.quote(master_status.getBuildbotURL(), '/:') 113 114 text += "Buildslave for this Build: %s\n\n" % build.getSlavename() 115 text += "Build Reason: %s\n" % build.getReason() 116 117 for ss in ss_list: 118 source = "" 119 if ss and ss.branch: 120 source += "[branch %s] " % ss.branch 121 if ss and ss.revision: 122 source += str(ss.revision) 123 else: 124 source += "HEAD" 125 if ss and ss.patch: 126 source += " (plus patch)" 127 128 discriminator = "" 129 if ss.codebase: 130 discriminator = " '%s'" % ss.codebase 131 text += "Build Source Stamp%s: %s\n" % (discriminator, source) 132 133 text += "Blamelist: %s\n" % ",".join(build.getResponsibleUsers()) 134 135 text += "\n" 136 137 t = build.getText() 138 if t: 139 t = ": " + " ".join(t) 140 else: 141 t = "" 142 143 if results == SUCCESS: 144 text += "Build succeeded!\n" 145 elif results == WARNINGS: 146 text += "Build Had Warnings%s\n" % t 147 else: 148 text += "BUILD FAILED%s\n" % t 149 150 text += "\n" 151 text += "sincerely,\n" 152 text += " -The Buildbot\n" 153 text += "\n" 154 return { 'body' : text, 'type' : 'plain' }
155
156 -class MailNotifier(base.StatusReceiverMultiService):
157 """This is a status notifier which sends email to a list of recipients 158 upon the completion of each build. It can be configured to only send out 159 mail for certain builds, and only send messages when the build fails, or 160 when it transitions from success to failure. It can also be configured to 161 include various build logs in each message. 162 163 By default, the message will be sent to the Interested Users list, which 164 includes all developers who made changes in the build. You can add 165 additional recipients with the extraRecipients argument. 166 167 To get a simple one-message-per-build (say, for a mailing list), use 168 sendToInterestedUsers=False, extraRecipients=['listaddr@example.org'] 169 170 Each MailNotifier sends mail to a single set of recipients. To send 171 different kinds of mail to different recipients, use multiple 172 MailNotifiers. 173 """ 174 175 implements(interfaces.IEmailSender) 176 177 compare_attrs = ["extraRecipients", "lookup", "fromaddr", "mode", 178 "categories", "builders", "addLogs", "relayhost", 179 "subject", "sendToInterestedUsers", "customMesg", 180 "messageFormatter", "extraHeaders"] 181 182 possible_modes = ("change", "failing", "passing", "problem", "warnings", "exception") 183
184 - def __init__(self, fromaddr, mode=("failing", "passing", "warnings"), 185 categories=None, builders=None, addLogs=False, 186 relayhost="localhost", buildSetSummary=False, 187 subject="buildbot %(result)s in %(title)s on %(builder)s", 188 lookup=None, extraRecipients=[], 189 sendToInterestedUsers=True, customMesg=None, 190 messageFormatter=defaultMessage, extraHeaders=None, 191 addPatch=True, useTls=False, 192 smtpUser=None, smtpPassword=None, smtpPort=25):
193 """ 194 @type fromaddr: string 195 @param fromaddr: the email address to be used in the 'From' header. 196 @type sendToInterestedUsers: boolean 197 @param sendToInterestedUsers: if True (the default), send mail to all 198 of the Interested Users. If False, only 199 send mail to the extraRecipients list. 200 201 @type extraRecipients: tuple of strings 202 @param extraRecipients: a list of email addresses to which messages 203 should be sent (in addition to the 204 InterestedUsers list, which includes any 205 developers who made Changes that went into this 206 build). It is a good idea to create a small 207 mailing list and deliver to that, then let 208 subscribers come and go as they please. The 209 addresses in this list are used literally (they 210 are not processed by lookup). 211 212 @type subject: string 213 @param subject: a string to be used as the subject line of the message. 214 %(builder)s will be replaced with the name of the 215 builder which provoked the message. 216 217 @type mode: list of strings 218 @param mode: a list of MailNotifer.possible_modes: 219 - "change": send mail about builds which change status 220 - "failing": send mail about builds which fail 221 - "passing": send mail about builds which succeed 222 - "problem": send mail about a build which failed 223 when the previous build passed 224 - "warnings": send mail if a build contain warnings 225 - "exception": send mail if a build fails due to an exception 226 - "all": always send mail 227 Defaults to ("failing", "passing", "warnings"). 228 229 @type builders: list of strings 230 @param builders: a list of builder names for which mail should be 231 sent. Defaults to None (send mail for all builds). 232 Use either builders or categories, but not both. 233 234 @type categories: list of strings 235 @param categories: a list of category names to serve status 236 information for. Defaults to None (all 237 categories). Use either builders or categories, 238 but not both. 239 240 @type addLogs: boolean 241 @param addLogs: if True, include all build logs as attachments to the 242 messages. These can be quite large. This can also be 243 set to a list of log names, to send a subset of the 244 logs. Defaults to False. 245 246 @type addPatch: boolean 247 @param addPatch: if True, include the patch when the source stamp 248 includes one. 249 250 @type relayhost: string 251 @param relayhost: the host to which the outbound SMTP connection 252 should be made. Defaults to 'localhost' 253 254 @type buildSetSummary: boolean 255 @param buildSetSummary: if True, this notifier will only send a summary 256 email when a buildset containing any of its 257 watched builds completes 258 259 @type lookup: implementor of {IEmailLookup} 260 @param lookup: object which provides IEmailLookup, which is 261 responsible for mapping User names for Interested 262 Users (which come from the VC system) into valid 263 email addresses. If not provided, the notifier will 264 only be able to send mail to the addresses in the 265 extraRecipients list. Most of the time you can use a 266 simple Domain instance. As a shortcut, you can pass 267 as string: this will be treated as if you had provided 268 Domain(str). For example, lookup='twistedmatrix.com' 269 will allow mail to be sent to all developers whose SVN 270 usernames match their twistedmatrix.com account names. 271 272 @type customMesg: func 273 @param customMesg: (this function is deprecated) 274 275 @type messageFormatter: func 276 @param messageFormatter: function taking (mode, name, build, result, 277 master_status) and returning a dictionary 278 containing two required keys "body" and "type", 279 with a third optional key, "subject". The 280 "body" key gives a string that contains the 281 complete text of the message. The "type" key 282 is the message type ('plain' or 'html'). The 283 'html' type should be used when generating an 284 HTML message. The optional "subject" key 285 gives the subject for the email. 286 287 @type extraHeaders: dict 288 @param extraHeaders: A dict of extra headers to add to the mail. It's 289 best to avoid putting 'To', 'From', 'Date', 290 'Subject', or 'CC' in here. Both the names and 291 values may be WithProperties instances. 292 293 @type useTls: boolean 294 @param useTls: Send emails using TLS and authenticate with the 295 smtp host. Defaults to False. 296 297 @type smtpUser: string 298 @param smtpUser: The user that will attempt to authenticate with the 299 relayhost when useTls is True. 300 301 @type smtpPassword: string 302 @param smtpPassword: The password that smtpUser will use when 303 authenticating with relayhost. 304 305 @type smtpPort: int 306 @param smtpPort: The port that will be used when connecting to the 307 relayhost. Defaults to 25. 308 """ 309 base.StatusReceiverMultiService.__init__(self) 310 311 if not isinstance(extraRecipients, (list, tuple)): 312 config.error("extraRecipients must be a list or tuple") 313 else: 314 for r in extraRecipients: 315 if not isinstance(r, str) or not VALID_EMAIL.search(r): 316 config.error( 317 "extra recipient %r is not a valid email" % (r,)) 318 self.extraRecipients = extraRecipients 319 self.sendToInterestedUsers = sendToInterestedUsers 320 self.fromaddr = fromaddr 321 if isinstance(mode, basestring): 322 if mode == "all": 323 mode = ("failing", "passing", "warnings", "exception") 324 elif mode == "warnings": 325 mode = ("failing", "warnings") 326 else: 327 mode = (mode,) 328 for m in mode: 329 if m not in self.possible_modes: 330 config.error( 331 "mode %s is not a valid mode" % (m,)) 332 self.mode = mode 333 self.categories = categories 334 self.builders = builders 335 self.addLogs = addLogs 336 self.relayhost = relayhost 337 if '\n' in subject: 338 config.error( 339 'Newlines are not allowed in email subjects') 340 self.subject = subject 341 if lookup is not None: 342 if type(lookup) is str: 343 lookup = Domain(lookup) 344 assert interfaces.IEmailLookup.providedBy(lookup) 345 self.lookup = lookup 346 self.customMesg = customMesg 347 self.messageFormatter = messageFormatter 348 if extraHeaders: 349 if not isinstance(extraHeaders, dict): 350 config.error("extraHeaders must be a dictionary") 351 self.extraHeaders = extraHeaders 352 self.addPatch = addPatch 353 self.useTls = useTls 354 self.smtpUser = smtpUser 355 self.smtpPassword = smtpPassword 356 self.smtpPort = smtpPort 357 self.buildSetSummary = buildSetSummary 358 self.buildSetSubscription = None 359 self.watched = [] 360 self.master_status = None 361 362 # you should either limit on builders or categories, not both 363 if self.builders != None and self.categories != None: 364 config.error( 365 "Please specify only builders or categories to include - " + 366 "not both.") 367 368 if customMesg: 369 config.error( 370 "customMesg is deprecated; use messageFormatter instead")
371
372 - def setServiceParent(self, parent):
373 """ 374 @type parent: L{buildbot.master.BuildMaster} 375 """ 376 base.StatusReceiverMultiService.setServiceParent(self, parent) 377 self.master_status = self.parent 378 self.master_status.subscribe(self) 379 self.master = self.master_status.master
380
381 - def startService(self):
382 if self.buildSetSummary: 383 self.buildSetSubscription = \ 384 self.master.subscribeToBuildsetCompletions(self.buildsetFinished) 385 386 base.StatusReceiverMultiService.startService(self)
387
388 - def stopService(self):
389 if self.buildSetSubscription is not None: 390 self.buildSetSubscription.unsubscribe() 391 self.buildSetSubscription = None 392 393 return base.StatusReceiverMultiService.stopService(self)
394
395 - def disownServiceParent(self):
396 self.master_status.unsubscribe(self) 397 self.master_status = None 398 for w in self.watched: 399 w.unsubscribe(self) 400 return base.StatusReceiverMultiService.disownServiceParent(self)
401
402 - def builderAdded(self, name, builder):
403 # only subscribe to builders we are interested in 404 if self.categories != None and builder.category not in self.categories: 405 return None 406 407 self.watched.append(builder) 408 return self # subscribe to this builder
409
410 - def builderRemoved(self, name):
411 pass
412
413 - def builderChangedState(self, name, state):
414 pass
415
416 - def buildStarted(self, name, build):
417 pass
418
419 - def isMailNeeded(self, build, results):
420 # here is where we actually do something. 421 builder = build.getBuilder() 422 if self.builders is not None and builder.name not in self.builders: 423 return False # ignore this build 424 if self.categories is not None and \ 425 builder.category not in self.categories: 426 return False # ignore this build 427 428 prev = build.getPreviousBuild() 429 if "change" in self.mode: 430 if prev and prev.getResults() != results: 431 return True 432 if "failing" in self.mode and results == FAILURE: 433 return True 434 if "passing" in self.mode and results == SUCCESS: 435 return True 436 if "problem" in self.mode and results == FAILURE: 437 if prev and prev.getResults() != FAILURE: 438 return True 439 if "warnings" in self.mode and results == WARNINGS: 440 return True 441 if "exception" in self.mode and results == EXCEPTION: 442 return True 443 444 return False
445
446 - def buildFinished(self, name, build, results):
447 if ( not self.buildSetSummary and 448 self.isMailNeeded(build, results) ): 449 # for testing purposes, buildMessage returns a Deferred that fires 450 # when the mail has been sent. To help unit tests, we return that 451 # Deferred here even though the normal IStatusReceiver.buildFinished 452 # signature doesn't do anything with it. If that changes (if 453 # .buildFinished's return value becomes significant), we need to 454 # rearrange this. 455 return self.buildMessage(name, [build], results) 456 return None
457
458 - def _gotBuilds(self, res, buildset):
459 builds = [] 460 for (builddictlist, builder) in res: 461 for builddict in builddictlist: 462 build = builder.getBuild(builddict['number']) 463 if build is not None and self.isMailNeeded(build, build.results): 464 builds.append(build) 465 466 if builds: 467 self.buildMessage("Buildset Complete: " + buildset['reason'], builds, 468 buildset['results'])
469
470 - def _gotBuildRequests(self, breqs, buildset):
471 dl = [] 472 for breq in breqs: 473 buildername = breq['buildername'] 474 builder = self.master_status.getBuilder(buildername) 475 d = self.master.db.builds.getBuildsForRequest(breq['brid']) 476 d.addCallback(lambda builddictlist, builder=builder: 477 (builddictlist, builder)) 478 dl.append(d) 479 d = defer.gatherResults(dl) 480 d.addCallback(self._gotBuilds, buildset)
481
482 - def _gotBuildSet(self, buildset, bsid):
483 d = self.master.db.buildrequests.getBuildRequests(bsid=bsid) 484 d.addCallback(self._gotBuildRequests, buildset)
485
486 - def buildsetFinished(self, bsid, result):
487 d = self.master.db.buildsets.getBuildset(bsid=bsid) 488 d.addCallback(self._gotBuildSet, bsid) 489 490 return d
491
492 - def getCustomMesgData(self, mode, name, build, results, master_status):
493 # 494 # logs is a list of tuples that contain the log 495 # name, log url, and the log contents as a list of strings. 496 # 497 logs = list() 498 for logf in build.getLogs(): 499 logStep = logf.getStep() 500 stepName = logStep.getName() 501 logStatus, dummy = logStep.getResults() 502 logName = logf.getName() 503 logs.append(('%s.%s' % (stepName, logName), 504 '%s/steps/%s/logs/%s' % ( 505 master_status.getURLForThing(build), 506 stepName, logName), 507 logf.getText().splitlines(), 508 logStatus)) 509 510 attrs = {'builderName': name, 511 'title': master_status.getTitle(), 512 'mode': mode, 513 'result': Results[results], 514 'buildURL': master_status.getURLForThing(build), 515 'buildbotURL': master_status.getBuildbotURL(), 516 'buildText': build.getText(), 517 'buildProperties': build.getProperties(), 518 'slavename': build.getSlavename(), 519 'reason': build.getReason().replace('\n', ''), 520 'responsibleUsers': build.getResponsibleUsers(), 521 'branch': "", 522 'revision': "", 523 'patch': "", 524 'patch_info': "", 525 'changes': [], 526 'logs': logs} 527 528 ss = None 529 ss_list = build.getSourceStamps() 530 531 if ss_list: 532 if len(ss_list) == 1: 533 ss = ss_list[0] 534 if ss: 535 attrs['branch'] = ss.branch 536 attrs['revision'] = ss.revision 537 attrs['patch'] = ss.patch 538 attrs['patch_info'] = ss.patch_info 539 attrs['changes'] = ss.changes[:] 540 else: 541 for key in ['branch', 'revision', 'patch', 'patch_info', 'changes']: 542 attrs[key] = {} 543 for ss in ss_list: 544 attrs['branch'][ss.codebase] = ss.branch 545 attrs['revision'][ss.codebase] = ss.revision 546 attrs['patch'][ss.codebase] = ss.patch 547 attrs['patch_info'][ss.codebase] = ss.patch_info 548 attrs['changes'][ss.codebase] = ss.changes[:] 549 550 return attrs
551
552 - def patch_to_attachment(self, patch, index):
553 # patches don't come with an encoding. If the patch is valid utf-8, 554 # we'll attach it as MIMEText; otherwise, it gets attached as a binary 555 # file. This will suit the vast majority of cases, since utf8 is by 556 # far the most common encoding. 557 if type(patch[1]) != types.UnicodeType: 558 try: 559 unicode = patch[1].decode('utf8') 560 except UnicodeDecodeError: 561 unicode = None 562 else: 563 unicode = patch[1] 564 565 if unicode: 566 a = MIMEText(unicode.encode(ENCODING), _charset=ENCODING) 567 else: 568 # MIMEApplication is not present in Python-2.4 :( 569 a = MIMENonMultipart('application', 'octet-stream') 570 a.set_payload(patch[1]) 571 a.add_header('Content-Disposition', "attachment", 572 filename="source patch " + str(index) ) 573 return a
574
575 - def createEmail(self, msgdict, builderName, title, results, builds=None, 576 patches=None, logs=None):
577 text = msgdict['body'].encode(ENCODING) 578 type = msgdict['type'] 579 if 'subject' in msgdict: 580 subject = msgdict['subject'].encode(ENCODING) 581 else: 582 subject = self.subject % { 'result': Results[results], 583 'projectName': title, 584 'title': title, 585 'builder': builderName, 586 } 587 588 assert '\n' not in subject, \ 589 "Subject cannot contain newlines" 590 591 assert type in ('plain', 'html'), \ 592 "'%s' message type must be 'plain' or 'html'." % type 593 594 if patches or logs: 595 m = MIMEMultipart() 596 m.attach(MIMEText(text, type, ENCODING)) 597 else: 598 m = Message() 599 m.set_payload(text, ENCODING) 600 m.set_type("text/%s" % type) 601 602 m['Date'] = formatdate(localtime=True) 603 m['Subject'] = subject 604 m['From'] = self.fromaddr 605 # m['To'] is added later 606 607 if patches: 608 for (i, patch) in enumerate(patches): 609 a = self.patch_to_attachment(patch, i) 610 m.attach(a) 611 if logs: 612 for log in logs: 613 name = "%s.%s" % (log.getStep().getName(), 614 log.getName()) 615 if ( self._shouldAttachLog(log.getName()) or 616 self._shouldAttachLog(name) ): 617 text = log.getText() 618 if not isinstance(text, unicode): 619 # guess at the encoding, and use replacement symbols 620 # for anything that's not in that encoding 621 text = text.decode(LOG_ENCODING, 'replace') 622 a = MIMEText(text.encode(ENCODING), 623 _charset=ENCODING) 624 a.add_header('Content-Disposition', "attachment", 625 filename=name) 626 m.attach(a) 627 628 #@todo: is there a better way to do this? 629 # Add any extra headers that were requested, doing WithProperties 630 # interpolation if only one build was given 631 if self.extraHeaders: 632 if len(builds) == 1: 633 d = builds[0].render(self.extraHeaders) 634 else: 635 d = defer.succeed(self.extraHeaders) 636 @d.addCallback 637 def addExtraHeaders(extraHeaders): 638 for k,v in extraHeaders.items(): 639 if k in m: 640 twlog.msg("Warning: Got header " + k + 641 " in self.extraHeaders " 642 "but it already exists in the Message - " 643 "not adding it.") 644 m[k] = v
645 d.addCallback(lambda _: m) 646 return d 647 648 return defer.succeed(m)
649
650 - def buildMessageDict(self, name, build, results):
651 if self.customMesg: 652 # the customMesg stuff can be *huge*, so we prefer not to load it 653 attrs = self.getCustomMesgData(self.mode, name, build, results, 654 self.master_status) 655 text, type = self.customMesg(attrs) 656 msgdict = { 'body' : text, 'type' : type } 657 else: 658 msgdict = self.messageFormatter(self.mode, name, build, results, 659 self.master_status) 660 661 return msgdict
662 663
664 - def buildMessage(self, name, builds, results):
665 patches = [] 666 logs = [] 667 msgdict = {"body":""} 668 669 for build in builds: 670 ss_list = build.getSourceStamps() 671 if self.addPatch: 672 for ss in ss_list: 673 if ss.patch: 674 patches.append(ss.patch) 675 if self.addLogs: 676 logs.extend(build.getLogs()) 677 678 tmp = self.buildMessageDict(name=build.getBuilder().name, 679 build=build, results=build.results) 680 msgdict['body'] += tmp['body'] 681 msgdict['body'] += '\n\n' 682 msgdict['type'] = tmp['type'] 683 if "subject" in tmp: 684 msgdict['subject'] = tmp['subject'] 685 686 d = self.createEmail(msgdict, name, self.master_status.getTitle(), 687 results, builds, patches, logs) 688 689 @d.addCallback 690 def getRecipients(m): 691 # now, who is this message going to? 692 if self.sendToInterestedUsers: 693 dl = [] 694 for build in builds: 695 if self.lookup: 696 d = self.useLookup(build) 697 else: 698 d = self.useUsers(build) 699 dl.append(d) 700 d = defer.gatherResults(dl) 701 else: 702 d = defer.succeed([]) 703 d.addCallback(self._gotRecipients, m)
704 return d 705
706 - def useLookup(self, build):
707 dl = [] 708 for u in build.getInterestedUsers(): 709 d = defer.maybeDeferred(self.lookup.getAddress, u) 710 dl.append(d) 711 return defer.gatherResults(dl)
712
713 - def useUsers(self, build):
714 dl = [] 715 ss = None 716 ss_list = build.getSourceStamps() 717 for ss in ss_list: 718 for change in ss.changes: 719 d = self.master.db.changes.getChangeUids(change.number) 720 def getContacts(uids): 721 def uidContactPair(contact, uid): 722 return (contact, uid)
723 contacts = [] 724 for uid in uids: 725 d = users.getUserContact(self.master, 726 contact_type='email', 727 uid=uid) 728 d.addCallback(lambda contact: uidContactPair(contact, uid)) 729 contacts.append(d) 730 return defer.gatherResults(contacts) 731 d.addCallback(getContacts) 732 def logNoMatch(contacts): 733 for pair in contacts: 734 contact, uid = pair 735 if contact is None: 736 twlog.msg("Unable to find email for uid: %r" % uid) 737 return [pair[0] for pair in contacts] 738 d.addCallback(logNoMatch) 739 def addOwners(recipients): 740 owners = [e for e in build.getInterestedUsers() 741 if e not in build.getResponsibleUsers()] 742 recipients.extend(owners) 743 return recipients 744 d.addCallback(addOwners) 745 dl.append(d) 746 d = defer.gatherResults(dl) 747 @d.addCallback 748 def gatherRecipients(res): 749 recipients = [] 750 map(recipients.extend, res) 751 return recipients 752 return d 753
754 - def _shouldAttachLog(self, logname):
755 if type(self.addLogs) is bool: 756 return self.addLogs 757 return logname in self.addLogs
758
759 - def _gotRecipients(self, rlist, m):
760 to_recipients = set() 761 cc_recipients = set() 762 763 for r in reduce(list.__add__, rlist, []): 764 if r is None: # getAddress didn't like this address 765 continue 766 767 # Git can give emails like 'User' <user@foo.com>@foo.com so check 768 # for two @ and chop the last 769 if r.count('@') > 1: 770 r = r[:r.rindex('@')] 771 772 if VALID_EMAIL.search(r): 773 to_recipients.add(r) 774 else: 775 twlog.msg("INVALID EMAIL: %r" + r) 776 777 # If we're sending to interested users put the extras in the 778 # CC list so they can tell if they are also interested in the 779 # change: 780 if self.sendToInterestedUsers and to_recipients: 781 cc_recipients.update(self.extraRecipients) 782 else: 783 to_recipients.update(self.extraRecipients) 784 785 m['To'] = ", ".join(sorted(to_recipients)) 786 if cc_recipients: 787 m['CC'] = ", ".join(sorted(cc_recipients)) 788 789 return self.sendMessage(m, list(to_recipients | cc_recipients))
790
791 - def sendmail(self, s, recipients):
792 result = defer.Deferred() 793 794 if have_ssl and self.useTls: 795 client_factory = ssl.ClientContextFactory() 796 client_factory.method = SSLv3_METHOD 797 else: 798 client_factory = None 799 800 if self.smtpUser and self.smtpPassword: 801 useAuth = True 802 else: 803 useAuth = False 804 805 if not ESMTPSenderFactory: 806 raise RuntimeError("twisted-mail is not installed - cannot " 807 "send mail") 808 sender_factory = ESMTPSenderFactory( 809 self.smtpUser, self.smtpPassword, 810 self.fromaddr, recipients, StringIO(s), 811 result, contextFactory=client_factory, 812 requireTransportSecurity=self.useTls, 813 requireAuthentication=useAuth) 814 815 reactor.connectTCP(self.relayhost, self.smtpPort, sender_factory) 816 817 return result
818
819 - def sendMessage(self, m, recipients):
820 s = m.as_string() 821 twlog.msg("sending mail (%d bytes) to" % len(s), recipients) 822 return self.sendmail(s, recipients)
823