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
32 """Generate a buildbot mail message and return a tuple of message text
33 and type.
34
35 This function can be replaced using the customMesg variable in MailNotifier.
36 A message function will *always* get a dictionary of attributes with
37 the following values:
38
39 builderName - (str) Name of the builder that generated this event.
40
41 projectName - (str) Name of the project.
42
43 mode - (str) Mode set in MailNotifier. (failing, passing, problem, change).
44
45 result - (str) Builder result as a string. 'success', 'warnings',
46 'failure', 'skipped', or 'exception'
47
48 buildURL - (str) URL to build page.
49
50 buildbotURL - (str) URL to buildbot main page.
51
52 buildText - (str) Build text from build.getText().
53
54 buildProperties - (Properties) Mapping of property names to values
55
56 slavename - (str) Slavename.
57
58 reason - (str) Build reason from build.getReason().
59
60 responsibleUsers - (List of str) List of responsible users.
61
62 branch - (str) Name of branch used. If no SourceStamp exists branch
63 is an empty string.
64
65 revision - (str) Name of revision used. If no SourceStamp exists revision
66 is an empty string.
67
68 patch - (str) Name of patch used. If no SourceStamp exists patch
69 is an empty string.
70
71 changes - (list of objs) List of change objects from SourceStamp. A change
72 object has the following useful information:
73
74 who - who made this change
75 revision - what VC revision is this change
76 branch - on what branch did this change occur
77 when - when did this change occur
78 files - what files were affected in this change
79 comments - comments reguarding the change.
80
81 The functions asText and asHTML return a list of strings with
82 the above information formatted.
83
84 logs - (List of Tuples) List of tuples that contain the log name, log url
85 and log contents as a list of strings.
86 """
87 text = ""
88 if attrs['mode'] == "all":
89 text += "The Buildbot has finished a build"
90 elif attrs['mode'] == "failing":
91 text += "The Buildbot has detected a failed build"
92 elif attrs['mode'] == "passing":
93 text += "The Buildbot has detected a passing build"
94 elif attrs['mode'] == "change" and attrs['result'] == 'success':
95 text += "The Buildbot has detected a restored build"
96 else:
97 text += "The Buildbot has detected a new failure"
98 text += " of %s on %s.\n" % (attrs['builderName'], attrs['projectName'])
99 if attrs['buildURL']:
100 text += "Full details are available at:\n %s\n" % attrs['buildURL']
101 text += "\n"
102
103 if attrs['buildbotURL']:
104 text += "Buildbot URL: %s\n\n" % urllib.quote(attrs['buildbotURL'], '/:')
105
106 text += "Buildslave for this Build: %s\n\n" % attrs['slavename']
107 text += "Build Reason: %s\n" % attrs['reason']
108
109
110
111
112 source = ""
113 if attrs['branch']:
114 source += "[branch %s] " % attrs['branch']
115 if attrs['revision']:
116 source += attrs['revision']
117 else:
118 source += "HEAD"
119 if attrs['patch']:
120 source += " (plus patch)"
121
122 text += "Build Source Stamp: %s\n" % source
123
124 text += "Blamelist: %s\n" % ",".join(attrs['responsibleUsers'])
125
126 text += "\n"
127
128 t = attrs['buildText']
129 if t:
130 t = ": " + " ".join(t)
131 else:
132 t = ""
133
134 if attrs['result'] == 'success':
135 text += "Build succeeded!\n"
136 elif attrs['result'] == 'warnings':
137 text += "Build Had Warnings%s\n" % t
138 else:
139 text += "BUILD FAILED%s\n" % t
140
141 text += "\n"
142 text += "sincerely,\n"
143 text += " -The Buildbot\n"
144 text += "\n"
145 return (text, 'plain')
146
147 -class Domain(util.ComparableMixin):
148 implements(interfaces.IEmailLookup)
149 compare_attrs = ["domain"]
150
151 - def __init__(self, domain):
152 assert "@" not in domain
153 self.domain = domain
154
155 - def getAddress(self, name):
156 """If name is already an email address, pass it through."""
157 if '@' in name:
158 return name
159 return name + "@" + self.domain
160
161
163 """This is a status notifier which sends email to a list of recipients
164 upon the completion of each build. It can be configured to only send out
165 mail for certain builds, and only send messages when the build fails, or
166 when it transitions from success to failure. It can also be configured to
167 include various build logs in each message.
168
169 By default, the message will be sent to the Interested Users list, which
170 includes all developers who made changes in the build. You can add
171 additional recipients with the extraRecipients argument.
172
173 To get a simple one-message-per-build (say, for a mailing list), use
174 sendToInterestedUsers=False, extraRecipients=['listaddr@example.org']
175
176 Each MailNotifier sends mail to a single set of recipients. To send
177 different kinds of mail to different recipients, use multiple
178 MailNotifiers.
179 """
180
181 implements(interfaces.IEmailSender)
182
183 compare_attrs = ["extraRecipients", "lookup", "fromaddr", "mode",
184 "categories", "builders", "addLogs", "relayhost",
185 "subject", "sendToInterestedUsers", "customMesg",
186 "extraHeaders"]
187
188 - def __init__(self, fromaddr, mode="all", categories=None, builders=None,
189 addLogs=False, relayhost="localhost",
190 subject="buildbot %(result)s in %(projectName)s on %(builder)s",
191 lookup=None, extraRecipients=[],
192 sendToInterestedUsers=True, customMesg=message,
193 extraHeaders=None):
194 """
195 @type fromaddr: string
196 @param fromaddr: the email address to be used in the 'From' header.
197 @type sendToInterestedUsers: boolean
198 @param sendToInterestedUsers: if True (the default), send mail to all
199 of the Interested Users. If False, only
200 send mail to the extraRecipients list.
201
202 @type extraRecipients: tuple of string
203 @param extraRecipients: a list of email addresses to which messages
204 should be sent (in addition to the
205 InterestedUsers list, which includes any
206 developers who made Changes that went into this
207 build). It is a good idea to create a small
208 mailing list and deliver to that, then let
209 subscribers come and go as they please.
210
211 @type subject: string
212 @param subject: a string to be used as the subject line of the message.
213 %(builder)s will be replaced with the name of the
214 builder which provoked the message.
215
216 @type mode: string (defaults to all)
217 @param mode: one of:
218 - 'all': send mail about all builds, passing and failing
219 - 'failing': only send mail about builds which fail
220 - 'passing': only send mail about builds which succeed
221 - 'problem': only send mail about a build which failed
222 when the previous build passed
223 - 'change': only send mail about builds who change status
224
225 @type builders: list of strings
226 @param builders: a list of builder names for which mail should be
227 sent. Defaults to None (send mail for all builds).
228 Use either builders or categories, but not both.
229
230 @type categories: list of strings
231 @param categories: a list of category names to serve status
232 information for. Defaults to None (all
233 categories). Use either builders or categories,
234 but not both.
235
236 @type addLogs: boolean.
237 @param addLogs: if True, include all build logs as attachments to the
238 messages. These can be quite large. This can also be
239 set to a list of log names, to send a subset of the
240 logs. Defaults to False.
241
242 @type relayhost: string
243 @param relayhost: the host to which the outbound SMTP connection
244 should be made. Defaults to 'localhost'
245
246 @type lookup: implementor of {IEmailLookup}
247 @param lookup: object which provides IEmailLookup, which is
248 responsible for mapping User names (which come from
249 the VC system) into valid email addresses. If not
250 provided, the notifier will only be able to send mail
251 to the addresses in the extraRecipients list. Most of
252 the time you can use a simple Domain instance. As a
253 shortcut, you can pass as string: this will be
254 treated as if you had provided Domain(str). For
255 example, lookup='twistedmatrix.com' will allow mail
256 to be sent to all developers whose SVN usernames
257 match their twistedmatrix.com account names.
258
259 @type customMesg: func
260 @param customMesg: A function that returns a tuple containing the text of
261 a custom message and its type. This function takes
262 the dict attrs which has the following values:
263
264 builderName - (str) Name of the builder that generated this event.
265
266 projectName - (str) Name of the project.
267
268 mode - (str) Mode set in MailNotifier. (failing, passing, problem, change).
269
270 result - (str) Builder result as a string. 'success', 'warnings',
271 'failure', 'skipped', or 'exception'
272
273 buildURL - (str) URL to build page.
274
275 buildbotURL - (str) URL to buildbot main page.
276
277 buildText - (str) Build text from build.getText().
278
279 buildProperties - (Properties instance) Mapping of
280 property names to values
281
282 slavename - (str) Slavename.
283
284 reason - (str) Build reason from build.getReason().
285
286 responsibleUsers - (List of str) List of responsible users.
287
288 branch - (str) Name of branch used. If no SourceStamp exists branch
289 is an empty string.
290
291 revision - (str) Name of revision used. If no SourceStamp exists revision
292 is an empty string.
293
294 patch - (str) Name of patch used. If no SourceStamp exists patch
295 is an empty string.
296
297 changes - (list of objs) List of change objects from SourceStamp. A change
298 object has the following useful information:
299
300 who - who made this change
301 revision - what VC revision is this change
302 branch - on what branch did this change occur
303 when - when did this change occur
304 files - what files were affected in this change
305 comments - comments reguarding the change.
306
307 The functions asText and asHTML return a list of strings with
308 the above information formatted.
309
310 logs - (List of Tuples) List of tuples that contain the log name, log url,
311 and log contents as a list of strings.
312 @type extraHeaders: dict
313 @param extraHeaders: A dict of extra headers to add to the mail. It's
314 best to avoid putting 'To', 'From', 'Date',
315 'Subject', or 'CC' in here. Both the names and
316 values may be WithProperties instances.
317 """
318
319 base.StatusReceiverMultiService.__init__(self)
320 assert isinstance(extraRecipients, (list, tuple))
321 for r in extraRecipients:
322 assert isinstance(r, str)
323 assert VALID_EMAIL.search(r)
324 self.extraRecipients = extraRecipients
325 self.sendToInterestedUsers = sendToInterestedUsers
326 self.fromaddr = fromaddr
327 assert mode in ('all', 'failing', 'problem', 'change')
328 self.mode = mode
329 self.categories = categories
330 self.builders = builders
331 self.addLogs = addLogs
332 self.relayhost = relayhost
333 self.subject = subject
334 if lookup is not None:
335 if type(lookup) is str:
336 lookup = Domain(lookup)
337 assert interfaces.IEmailLookup.providedBy(lookup)
338 self.lookup = lookup
339 self.customMesg = customMesg
340 if extraHeaders:
341 assert isinstance(extraHeaders, dict)
342 self.extraHeaders = extraHeaders
343 self.watched = []
344 self.status = None
345
346
347 if self.builders != None and self.categories != None:
348 twlog.err("Please specify only builders to ignore or categories to include")
349 raise
350
357
361
367
369
370 if self.categories != None and builder.category not in self.categories:
371 return None
372
373 self.watched.append(builder)
374 return self
375
378
417
419
420
421
422
423 logs = list()
424 for logf in build.getLogs():
425 logStep = logf.getStep()
426 stepName = logStep.getName()
427 logStatus, dummy = logStep.getResults()
428 logName = logf.getName()
429 logs.append(('%s.%s' % (stepName, logName),
430 '%s/steps/%s/logs/%s' % (self.status.getURLForThing(build), stepName, logName),
431 logf.getText().splitlines(),
432 logStatus))
433
434 properties = build.getProperties()
435
436 attrs = {'builderName': name,
437 'projectName': self.status.getProjectName(),
438 'mode': self.mode,
439 'result': Results[results],
440 'buildURL': self.status.getURLForThing(build),
441 'buildbotURL': self.status.getBuildbotURL(),
442 'buildText': build.getText(),
443 'buildProperties': properties,
444 'slavename': build.getSlavename(),
445 'reason': build.getReason(),
446 'responsibleUsers': build.getResponsibleUsers(),
447 'branch': "",
448 'revision': "",
449 'patch': "",
450 'changes': [],
451 'logs': logs}
452
453 ss = build.getSourceStamp()
454 if ss:
455 attrs['branch'] = ss.branch
456 attrs['revision'] = ss.revision
457 attrs['patch'] = ss.patch
458 attrs['changes'] = ss.changes[:]
459
460 text, type = self.customMesg(attrs)
461 assert type in ('plain', 'html'), "'%s' message type must be 'plain' or 'html'." % type
462
463 haveAttachments = False
464 if attrs['patch'] or self.addLogs:
465 haveAttachments = True
466 if not canDoAttachments:
467 twlog.msg("warning: I want to send mail with attachments, "
468 "but this python is too old to have "
469 "email.MIMEMultipart . Please upgrade to python-2.3 "
470 "or newer to enable addLogs=True")
471
472 if haveAttachments and canDoAttachments:
473 m = MIMEMultipart()
474 m.attach(MIMEText(text, type))
475 else:
476 m = Message()
477 m.set_payload(text)
478 m.set_type("text/%s" % type)
479
480 m['Date'] = formatdate(localtime=True)
481 m['Subject'] = self.subject % { 'result': attrs['result'],
482 'projectName': attrs['projectName'],
483 'builder': attrs['builderName'],
484 }
485 m['From'] = self.fromaddr
486
487
488 if attrs['patch']:
489 a = MIMEText(attrs['patch'][1])
490 a.add_header('Content-Disposition', "attachment",
491 filename="source patch")
492 m.attach(a)
493 if self.addLogs:
494 for log in build.getLogs():
495 name = "%s.%s" % (log.getStep().getName(),
496 log.getName())
497 if self._shouldAttachLog(log.getName()) or self._shouldAttachLog(name):
498 a = MIMEText(log.getText())
499 a.add_header('Content-Disposition', "attachment",
500 filename=name)
501 m.attach(a)
502
503
504
505 if self.extraHeaders:
506 for k,v in self.extraHeaders.items():
507 k = properties.render(k)
508 if k in m:
509 twlog("Warning: Got header " + k + " in self.extraHeaders "
510 "but it already exists in the Message - "
511 "not adding it.")
512 continue
513 m[k] = properties.render(v)
514
515
516 dl = []
517 recipients = []
518 if self.sendToInterestedUsers and self.lookup:
519 for u in build.getInterestedUsers():
520 d = defer.maybeDeferred(self.lookup.getAddress, u)
521 d.addCallback(recipients.append)
522 dl.append(d)
523 d = defer.DeferredList(dl)
524 d.addCallback(self._gotRecipients, recipients, m)
525 return d
526
528 if type(self.addLogs) is bool:
529 return self.addLogs
530 return logname in self.addLogs
531
533 recipients = set()
534
535 for r in rlist:
536 if r is None:
537 continue
538
539
540
541 if r.count('@') > 1:
542 r = r[:r.rindex('@')]
543
544 if VALID_EMAIL.search(r):
545 recipients.add(r)
546 else:
547 twlog.msg("INVALID EMAIL: %r" + r)
548
549
550
551
552 if self.sendToInterestedUsers and len(recipients):
553 extra_recips = self.extraRecipients[:]
554 extra_recips.sort()
555 m['CC'] = ", ".join(extra_recips)
556 else:
557 [recipients.add(r) for r in self.extraRecipients[:]]
558
559 rlist = list(recipients)
560 rlist.sort()
561 m['To'] = ", ".join(rlist)
562
563
564 if self.sendToInterestedUsers:
565 for r in self.extraRecipients:
566 recipients.add(r)
567
568 return self.sendMessage(m, list(recipients))
569
571 s = m.as_string()
572 twlog.msg("sending mail (%d bytes) to" % len(s), recipients)
573 return sendmail(self.relayhost, self.fromaddr, recipients, s)
574