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