1   
  2   
  3   
  4  import re 
  5   
  6  from email.Message import Message 
  7  from email.Utils import formatdate 
  8  from email.MIMEText import MIMEText 
  9  try: 
 10      from email.MIMEMultipart import MIMEMultipart 
 11      canDoAttachments = True 
 12  except ImportError: 
 13      canDoAttachments = False 
 14  import urllib 
 15   
 16  from zope.interface import implements 
 17  from twisted.internet import defer 
 18  from twisted.mail.smtp import sendmail 
 19  from twisted.python import log as twlog 
 20   
 21  from buildbot import interfaces, util 
 22  from buildbot.status import base 
 23  from buildbot.status.builder import FAILURE, SUCCESS, WARNINGS, Results 
 24   
 25  import sys 
 26  if sys.version_info[:3] < (2,4,0): 
 27      from sets import Set as set 
 28   
 29  VALID_EMAIL = re.compile("[a-zA-Z0-9\.\_\%\-\+]+@[a-zA-Z0-9\.\_\%\-]+.[a-zA-Z]{2,6}") 
 30   
 31 -class Domain(util.ComparableMixin): 
  32      implements(interfaces.IEmailLookup) 
 33      compare_attrs = ["domain"] 
 34   
 35 -    def __init__(self, domain): 
  36          assert "@" not in domain 
 37          self.domain = domain 
  38   
 39 -    def getAddress(self, name): 
  40          """If name is already an email address, pass it through.""" 
 41          if '@' in name: 
 42              return name 
 43          return name + "@" + self.domain 
   44   
 45   
 47      """This is a status notifier which sends email to a list of recipients 
 48      upon the completion of each build. It can be configured to only send out 
 49      mail for certain builds, and only send messages when the build fails, or 
 50      when it transitions from success to failure. It can also be configured to 
 51      include various build logs in each message. 
 52   
 53      By default, the message will be sent to the Interested Users list, which 
 54      includes all developers who made changes in the build. You can add 
 55      additional recipients with the extraRecipients argument. 
 56   
 57      To get a simple one-message-per-build (say, for a mailing list), use 
 58      sendToInterestedUsers=False, extraRecipients=['listaddr@example.org'] 
 59   
 60      Each MailNotifier sends mail to a single set of recipients. To send 
 61      different kinds of mail to different recipients, use multiple 
 62      MailNotifiers. 
 63      """ 
 64   
 65      implements(interfaces.IEmailSender) 
 66   
 67      compare_attrs = ["extraRecipients", "lookup", "fromaddr", "mode", 
 68                       "categories", "builders", "addLogs", "relayhost", 
 69                       "subject", "sendToInterestedUsers", "customMesg", 
 70                       "messageFormatter", "extraHeaders"] 
 71   
 72 -    def __init__(self, fromaddr, mode="all", categories=None, builders=None, 
 73                   addLogs=False, relayhost="localhost", 
 74                   subject="buildbot %(result)s in %(projectName)s on %(builder)s", 
 75                   lookup=None, extraRecipients=[], 
 76                   sendToInterestedUsers=True, customMesg=None, 
 77                   messageFormatter=None, extraHeaders=None, addPatch=True): 
  78          """ 
 79          @type  fromaddr: string 
 80          @param fromaddr: the email address to be used in the 'From' header. 
 81          @type  sendToInterestedUsers: boolean 
 82          @param sendToInterestedUsers: if True (the default), send mail to all  
 83                                        of the Interested Users. If False, only 
 84                                        send mail to the extraRecipients list. 
 85   
 86          @type  extraRecipients: tuple of string 
 87          @param extraRecipients: a list of email addresses to which messages 
 88                                  should be sent (in addition to the 
 89                                  InterestedUsers list, which includes any 
 90                                  developers who made Changes that went into this 
 91                                  build). It is a good idea to create a small 
 92                                  mailing list and deliver to that, then let 
 93                                  subscribers come and go as they please. 
 94   
 95          @type  subject: string 
 96          @param subject: a string to be used as the subject line of the message. 
 97                          %(builder)s will be replaced with the name of the 
 98                          builder which provoked the message. 
 99   
100          @type  mode: string (defaults to all) 
101          @param mode: one of: 
102                       - 'all': send mail about all builds, passing and failing 
103                       - 'failing': only send mail about builds which fail 
104                       - 'passing': only send mail about builds which succeed 
105                       - 'problem': only send mail about a build which failed 
106                       when the previous build passed 
107                       - 'change': only send mail about builds who change status 
108   
109          @type  builders: list of strings 
110          @param builders: a list of builder names for which mail should be 
111                           sent. Defaults to None (send mail for all builds). 
112                           Use either builders or categories, but not both. 
113   
114          @type  categories: list of strings 
115          @param categories: a list of category names to serve status 
116                             information for. Defaults to None (all 
117                             categories). Use either builders or categories, 
118                             but not both. 
119   
120          @type  addLogs: boolean 
121          @param addLogs: if True, include all build logs as attachments to the 
122                          messages.  These can be quite large. This can also be 
123                          set to a list of log names, to send a subset of the 
124                          logs. Defaults to False. 
125   
126          @type  addPatch: boolean 
127          @param addPatch: if True, include the patch when the source stamp 
128                           includes one. 
129   
130          @type  relayhost: string 
131          @param relayhost: the host to which the outbound SMTP connection 
132                            should be made. Defaults to 'localhost' 
133   
134          @type  lookup:    implementor of {IEmailLookup} 
135          @param lookup:    object which provides IEmailLookup, which is 
136                            responsible for mapping User names (which come from 
137                            the VC system) into valid email addresses. If not 
138                            provided, the notifier will only be able to send mail 
139                            to the addresses in the extraRecipients list. Most of 
140                            the time you can use a simple Domain instance. As a 
141                            shortcut, you can pass as string: this will be 
142                            treated as if you had provided Domain(str). For 
143                            example, lookup='twistedmatrix.com' will allow mail 
144                            to be sent to all developers whose SVN usernames 
145                            match their twistedmatrix.com account names. 
146                             
147          @type  customMesg: func 
148          @param customMesg: (this function is deprecated) 
149   
150          @type  messageFormatter: func 
151          @param messageFormatter: function taking (mode, name, build, result, 
152                                   master_status ) and returning a dictionary 
153                                   containing two required keys "body" and "type", 
154                                   with a third optional key, "subject". The 
155                                   "body" key gives a string that contains the 
156                                   complete text of the message. The "type" key 
157                                   is the message type ('plain' or 'html'). The 
158                                   'html' type should be used when generating an 
159                                   HTML message.  The optional "subject" key 
160                                   gives the subject for the email. 
161   
162          @type  extraHeaders: dict 
163          @param extraHeaders: A dict of extra headers to add to the mail. It's 
164                               best to avoid putting 'To', 'From', 'Date', 
165                               'Subject', or 'CC' in here. Both the names and 
166                               values may be WithProperties instances. 
167          """ 
168   
169          base.StatusReceiverMultiService.__init__(self) 
170          assert isinstance(extraRecipients, (list, tuple)) 
171          for r in extraRecipients: 
172              assert isinstance(r, str) 
173              assert VALID_EMAIL.search(r)  
174          self.extraRecipients = extraRecipients 
175          self.sendToInterestedUsers = sendToInterestedUsers 
176          self.fromaddr = fromaddr 
177          assert mode in ('all', 'failing', 'problem', 'change', 'passing') 
178          self.mode = mode 
179          self.categories = categories 
180          self.builders = builders 
181          self.addLogs = addLogs 
182          self.relayhost = relayhost 
183          self.subject = subject 
184          if lookup is not None: 
185              if type(lookup) is str: 
186                  lookup = Domain(lookup) 
187              assert interfaces.IEmailLookup.providedBy(lookup) 
188          self.lookup = lookup 
189          self.customMesg = customMesg 
190          self.messageFormatter = messageFormatter 
191          if extraHeaders: 
192              assert isinstance(extraHeaders, dict) 
193          self.extraHeaders = extraHeaders 
194          self.addPatch = addPatch 
195          self.watched = [] 
196          self.master_status = None 
197   
198           
199          if self.builders != None and self.categories != None: 
200              twlog.err("Please specify only builders to ignore or categories to include") 
201              raise  
202   
203          if customMesg and messageFormatter: 
204              twlog.err("Specify only one of customMesg and messageFormatter") 
205              self.customMesg = None 
206   
207          if customMesg: 
208              twlog.msg("customMesg is deprecated; please use messageFormatter instead") 
 209   
216   
220   
226   
228           
229          if self.categories != None and builder.category not in self.categories: 
230              return None 
231   
232          self.watched.append(builder) 
233          return self  
 234   
237   
276   
278           
279           
280           
281           
282          logs = list() 
283          for logf in build.getLogs(): 
284              logStep = logf.getStep() 
285              stepName = logStep.getName() 
286              logStatus, dummy = logStep.getResults() 
287              logName = logf.getName() 
288              logs.append(('%s.%s' % (stepName, logName), 
289                           '%s/steps/%s/logs/%s' % (master_status.getURLForThing(build), stepName, logName), 
290                           logf.getText().splitlines(), 
291                           logStatus)) 
292   
293          properties = build.getProperties() 
294                   
295          attrs = {'builderName': name, 
296                   'projectName': master_status.getProjectName(), 
297                   'mode': mode, 
298                   'result': Results[results], 
299                   'buildURL': master_status.getURLForThing(build), 
300                   'buildbotURL': master_status.getBuildbotURL(), 
301                   'buildText': build.getText(), 
302                   'buildProperties': properties, 
303                   'slavename': build.getSlavename(), 
304                   'reason':  build.getReason(), 
305                   'responsibleUsers': build.getResponsibleUsers(), 
306                   'branch': "", 
307                   'revision': "", 
308                   'patch': "", 
309                   'changes': [], 
310                   'logs': logs} 
311   
312          ss = build.getSourceStamp() 
313          if ss: 
314              attrs['branch'] = ss.branch 
315              attrs['revision'] = ss.revision 
316              attrs['patch'] = ss.patch 
317              attrs['changes'] = ss.changes[:] 
318   
319          return attrs 
 320   
322          """Generate a buildbot mail message and return a tuple of message text 
323          and type.""" 
324          result = Results[results] 
325   
326          text = "" 
327          if mode == "all": 
328              text += "The Buildbot has finished a build" 
329          elif mode == "failing": 
330              text += "The Buildbot has detected a failed build" 
331          elif mode == "passing": 
332              text += "The Buildbot has detected a passing build" 
333          elif mode == "change" and result == 'success': 
334              text += "The Buildbot has detected a restored build" 
335          else:     
336              text += "The Buildbot has detected a new failure" 
337          text += " of %s on %s.\n" % (name, master_status.getProjectName()) 
338          if master_status.getURLForThing(build): 
339              text += "Full details are available at:\n %s\n" % master_status.getURLForThing(build) 
340          text += "\n" 
341   
342          if master_status.getBuildbotURL(): 
343              text += "Buildbot URL: %s\n\n" % urllib.quote(master_status.getBuildbotURL(), '/:') 
344   
345          text += "Buildslave for this Build: %s\n\n" % build.getSlavename() 
346          text += "Build Reason: %s\n" % build.getReason() 
347   
348          source = "" 
349          ss = build.getSourceStamp() 
350          if ss and ss.branch: 
351              source += "[branch %s] " % ss.branch 
352          if ss and ss.revision: 
353              source += str(ss.revision) 
354          else: 
355              source += "HEAD" 
356          if ss and ss.patch: 
357              source += " (plus patch)" 
358   
359          text += "Build Source Stamp: %s\n" % source 
360   
361          text += "Blamelist: %s\n" % ",".join(build.getResponsibleUsers()) 
362   
363          text += "\n" 
364   
365          t = build.getText() 
366          if t: 
367              t = ": " + " ".join(t) 
368          else: 
369              t = "" 
370   
371          if result == 'success': 
372              text += "Build succeeded!\n" 
373          elif result == 'warnings': 
374              text += "Build Had Warnings%s\n" % t 
375          else: 
376              text += "BUILD FAILED%s\n" % t 
377   
378          text += "\n" 
379          text += "sincerely,\n" 
380          text += " -The Buildbot\n" 
381          text += "\n" 
382          return { 'body' : text, 'type' : 'plain' } 
 383   
385          if self.customMesg: 
386               
387              attrs = self.getCustomMesgData(self.mode, name, build, results, self.master_status) 
388              text, type = self.customMesg(attrs) 
389              msgdict = { 'body' : text, 'type' : type } 
390          elif self.messageFormatter: 
391              msgdict = self.messageFormatter(self.mode, name, build, results, self.master_status) 
392          else: 
393              msgdict = self.defaultMessage(self.mode, name, build, results, self.master_status) 
394   
395          text = msgdict['body'] 
396          type = msgdict['type'] 
397          if 'subject' in msgdict: 
398              subject = msgdict['subject'] 
399          else: 
400              subject = self.subject % { 'result': Results[results], 
401                                         'projectName': self.master_status.getProjectName(), 
402                                         'builder': name, 
403                                         } 
404   
405   
406          assert type in ('plain', 'html'), "'%s' message type must be 'plain' or 'html'." % type 
407   
408          haveAttachments = False 
409          ss = build.getSourceStamp() 
410          if (ss and ss.patch and self.addPatch) or self.addLogs: 
411              haveAttachments = True 
412              if not canDoAttachments: 
413                  twlog.msg("warning: I want to send mail with attachments, " 
414                            "but this python is too old to have " 
415                            "email.MIMEMultipart . Please upgrade to python-2.3 " 
416                            "or newer to enable addLogs=True") 
417   
418          if haveAttachments and canDoAttachments: 
419              m = MIMEMultipart() 
420              m.attach(MIMEText(text, type)) 
421          else: 
422              m = Message() 
423              m.set_payload(text) 
424              m.set_type("text/%s" % type) 
425   
426          m['Date'] = formatdate(localtime=True) 
427          m['Subject'] = subject 
428          m['From'] = self.fromaddr 
429           
430   
431          if ss and ss.patch and self.addPatch: 
432              patch = ss.patch 
433              a = MIMEText(patch[1]) 
434              a.add_header('Content-Disposition', "attachment", 
435                           filename="source patch") 
436              m.attach(a) 
437          if self.addLogs: 
438              for log in build.getLogs(): 
439                  name = "%s.%s" % (log.getStep().getName(), 
440                                    log.getName()) 
441                  if self._shouldAttachLog(log.getName()) or self._shouldAttachLog(name): 
442                      a = MIMEText(log.getText()) 
443                      a.add_header('Content-Disposition', "attachment", 
444                                   filename=name) 
445                      m.attach(a) 
446   
447           
448           
449          if self.extraHeaders: 
450              for k,v in self.extraHeaders.items(): 
451                  k = properties.render(k) 
452                  if k in m: 
453                      twlog("Warning: Got header " + k + " in self.extraHeaders " 
454                            "but it already exists in the Message - " 
455                            "not adding it.") 
456                      continue 
457                  m[k] = properties.render(v) 
458   
459           
460          dl = [] 
461          recipients = [] 
462          if self.sendToInterestedUsers and self.lookup: 
463              for u in build.getInterestedUsers(): 
464                  d = defer.maybeDeferred(self.lookup.getAddress, u) 
465                  d.addCallback(recipients.append) 
466                  dl.append(d) 
467          d = defer.DeferredList(dl) 
468          d.addCallback(self._gotRecipients, recipients, m) 
469          return d 
 470   
472          if type(self.addLogs) is bool: 
473              return self.addLogs 
474          return logname in self.addLogs 
 475   
477          recipients = set() 
478   
479          for r in rlist: 
480              if r is None:  
481                  continue 
482   
483               
484               
485              if r.count('@') > 1: 
486                  r = r[:r.rindex('@')] 
487   
488              if VALID_EMAIL.search(r): 
489                  recipients.add(r) 
490              else: 
491                  twlog.msg("INVALID EMAIL: %r" + r) 
492   
493           
494           
495           
496          if self.sendToInterestedUsers and len(recipients): 
497              extra_recips = self.extraRecipients[:] 
498              extra_recips.sort() 
499              m['CC'] = ", ".join(extra_recips) 
500          else: 
501              [recipients.add(r) for r in self.extraRecipients[:]] 
502   
503          rlist = list(recipients) 
504          rlist.sort() 
505          m['To'] = ", ".join(rlist) 
506   
507           
508          if self.sendToInterestedUsers: 
509              for r in self.extraRecipients: 
510                  recipients.add(r) 
511   
512          return self.sendMessage(m, list(recipients)) 
 513   
515          s = m.as_string() 
516          twlog.msg("sending mail (%d bytes) to" % len(s), recipients) 
517          return sendmail(self.relayhost, self.fromaddr, recipients, s) 
  518