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