1 # -*- test-case-name: buildbot.test.test_status -*-
3 # the email.MIMEMultipart module is only available in python-2.2.2 and later
5 from email
.Message
import Message
6 from email
.Utils
import formatdate
7 from email
.MIMEText
import MIMEText
9 from email
.MIMEMultipart
import MIMEMultipart
10 canDoAttachments
= True
12 canDoAttachments
= False
15 from zope
.interface
import implements
16 from twisted
.internet
import defer
17 from twisted
.mail
.smtp
import sendmail
18 from twisted
.python
import log
as twlog
20 from buildbot
import interfaces
, util
21 from buildbot
.status
import base
22 from buildbot
.status
.builder
import FAILURE
, SUCCESS
, WARNINGS
25 class Domain(util
.ComparableMixin
):
26 implements(interfaces
.IEmailLookup
)
27 compare_attrs
= ["domain"]
29 def __init__(self
, domain
):
30 assert "@" not in domain
33 def getAddress(self
, name
):
34 return name
+ "@" + self
.domain
37 class MailNotifier(base
.StatusReceiverMultiService
):
38 """This is a status notifier which sends email to a list of recipients
39 upon the completion of each build. It can be configured to only send out
40 mail for certain builds, and only send messages when the build fails, or
41 when it transitions from success to failure. It can also be configured to
42 include various build logs in each message.
44 By default, the message will be sent to the Interested Users list, which
45 includes all developers who made changes in the build. You can add
46 additional recipients with the extraRecipients argument.
48 To get a simple one-message-per-build (say, for a mailing list), use
49 sendToInterestedUsers=False, extraRecipients=['listaddr@example.org']
51 Each MailNotifier sends mail to a single set of recipients. To send
52 different kinds of mail to different recipients, use multiple
56 implements(interfaces
.IEmailSender
)
58 compare_attrs
= ["extraRecipients", "lookup", "fromaddr", "mode",
59 "categories", "builders", "addLogs", "relayhost",
60 "subject", "sendToInterestedUsers"]
62 def __init__(self
, fromaddr
, mode
="all", categories
=None, builders
=None,
63 addLogs
=False, relayhost
="localhost",
64 subject
="buildbot %(result)s in %(projectName)s on %(builder)s",
65 lookup
=None, extraRecipients
=[],
66 sendToInterestedUsers
=True):
68 @type fromaddr: string
69 @param fromaddr: the email address to be used in the 'From' header.
70 @type sendToInterestedUsers: boolean
71 @param sendToInterestedUsers: if True (the default), send mail to all
72 of the Interested Users. If False, only
73 send mail to the extraRecipients list.
75 @type extraRecipients: tuple of string
76 @param extraRecipients: a list of email addresses to which messages
77 should be sent (in addition to the
78 InterestedUsers list, which includes any
79 developers who made Changes that went into this
80 build). It is a good idea to create a small
81 mailing list and deliver to that, then let
82 subscribers come and go as they please.
85 @param subject: a string to be used as the subject line of the message.
86 %(builder)s will be replaced with the name of the
87 builder which provoked the message.
89 @type mode: string (defaults to all)
91 - 'all': send mail about all builds, passing and failing
92 - 'failing': only send mail about builds which fail
93 - 'passing': only send mail about builds which succeed
94 - 'problem': only send mail about a build which failed
95 when the previous build passed
97 @type builders: list of strings
98 @param builders: a list of builder names for which mail should be
99 sent. Defaults to None (send mail for all builds).
100 Use either builders or categories, but not both.
102 @type categories: list of strings
103 @param categories: a list of category names to serve status
104 information for. Defaults to None (all
105 categories). Use either builders or categories,
108 @type addLogs: boolean.
109 @param addLogs: if True, include all build logs as attachments to the
110 messages. These can be quite large. This can also be
111 set to a list of log names, to send a subset of the
112 logs. Defaults to False.
114 @type relayhost: string
115 @param relayhost: the host to which the outbound SMTP connection
116 should be made. Defaults to 'localhost'
118 @type lookup: implementor of {IEmailLookup}
119 @param lookup: object which provides IEmailLookup, which is
120 responsible for mapping User names (which come from
121 the VC system) into valid email addresses. If not
122 provided, the notifier will only be able to send mail
123 to the addresses in the extraRecipients list. Most of
124 the time you can use a simple Domain instance. As a
125 shortcut, you can pass as string: this will be
126 treated as if you had provided Domain(str). For
127 example, lookup='twistedmatrix.com' will allow mail
128 to be sent to all developers whose SVN usernames
129 match their twistedmatrix.com account names.
132 base
.StatusReceiverMultiService
.__init
__(self
)
133 assert isinstance(extraRecipients
, (list, tuple))
134 for r
in extraRecipients
:
135 assert isinstance(r
, str)
136 assert "@" in r
# require full email addresses, not User names
137 self
.extraRecipients
= extraRecipients
138 self
.sendToInterestedUsers
= sendToInterestedUsers
139 self
.fromaddr
= fromaddr
140 assert mode
in ('all', 'failing', 'problem')
142 self
.categories
= categories
143 self
.builders
= builders
144 self
.addLogs
= addLogs
145 self
.relayhost
= relayhost
146 self
.subject
= subject
147 if lookup
is not None:
148 if type(lookup
) is str:
149 lookup
= Domain(lookup
)
150 assert interfaces
.IEmailLookup
.providedBy(lookup
)
155 # you should either limit on builders or categories, not both
156 if self
.builders
!= None and self
.categories
!= None:
157 twlog
.err("Please specify only builders to ignore or categories to include")
158 raise # FIXME: the asserts above do not raise some Exception either
160 def setServiceParent(self
, parent
):
162 @type parent: L{buildbot.master.BuildMaster}
164 base
.StatusReceiverMultiService
.setServiceParent(self
, parent
)
168 self
.status
= self
.parent
.getStatus()
169 self
.status
.subscribe(self
)
171 def disownServiceParent(self
):
172 self
.status
.unsubscribe(self
)
173 for w
in self
.watched
:
175 return base
.StatusReceiverMultiService
.disownServiceParent(self
)
177 def builderAdded(self
, name
, builder
):
178 # only subscribe to builders we are interested in
179 if self
.categories
!= None and builder
.category
not in self
.categories
:
182 self
.watched
.append(builder
)
183 return self
# subscribe to this builder
185 def builderRemoved(self
, name
):
188 def builderChangedState(self
, name
, state
):
190 def buildStarted(self
, name
, build
):
192 def buildFinished(self
, name
, build
, results
):
193 # here is where we actually do something.
194 builder
= build
.getBuilder()
195 if self
.builders
is not None and name
not in self
.builders
:
196 return # ignore this build
197 if self
.categories
is not None and \
198 builder
.category
not in self
.categories
:
199 return # ignore this build
201 if self
.mode
== "failing" and results
!= FAILURE
:
203 if self
.mode
== "passing" and results
!= SUCCESS
:
205 if self
.mode
== "problem":
206 if results
!= FAILURE
:
208 prev
= build
.getPreviousBuild()
209 if prev
and prev
.getResults() == FAILURE
:
211 # for testing purposes, buildMessage returns a Deferred that fires
212 # when the mail has been sent. To help unit tests, we return that
213 # Deferred here even though the normal IStatusReceiver.buildFinished
214 # signature doesn't do anything with it. If that changes (if
215 # .buildFinished's return value becomes significant), we need to
217 return self
.buildMessage(name
, build
, results
)
219 def buildMessage(self
, name
, build
, results
):
220 projectName
= self
.status
.getProjectName()
222 if self
.mode
== "all":
223 text
+= "The Buildbot has finished a build"
224 elif self
.mode
== "failing":
225 text
+= "The Buildbot has detected a failed build"
226 elif self
.mode
== "passing":
227 text
+= "The Buildbot has detected a passing build"
229 text
+= "The Buildbot has detected a new failure"
230 text
+= " of %s on %s.\n" % (name
, projectName
)
231 buildurl
= self
.status
.getURLForThing(build
)
233 text
+= "Full details are available at:\n %s\n" % buildurl
236 url
= self
.status
.getBuildbotURL()
238 text
+= "Buildbot URL: %s\n\n" % urllib
.quote(url
, '/:')
240 text
+= "Buildslave for this Build: %s\n\n" % build
.getSlavename()
241 text
+= "Build Reason: %s\n" % build
.getReason()
244 ss
= build
.getSourceStamp()
246 source
= "unavailable"
250 source
+= "[branch %s] " % ss
.branch
252 source
+= ss
.revision
255 if ss
.patch
is not None:
256 source
+= " (plus patch)"
258 text
+= "Build Source Stamp: %s\n" % source
260 text
+= "Blamelist: %s\n" % ",".join(build
.getResponsibleUsers())
262 # TODO: maybe display changes here? or in an attachment?
267 t
= ": " + " ".join(t
)
271 if results
== SUCCESS
:
272 text
+= "Build succeeded!\n"
274 elif results
== WARNINGS
:
275 text
+= "Build Had Warnings%s\n" % t
278 text
+= "BUILD FAILED%s\n" % t
281 if self
.addLogs
and build
.getLogs():
282 text
+= "Logs are attached.\n"
284 # TODO: it would be nice to provide a URL for the specific build
285 # here. That involves some coordination with html.Waterfall .
286 # Ideally we could do:
287 # helper = self.parent.getServiceNamed("html")
289 # url = helper.getURLForBuild(build)
292 text
+= "sincerely,\n"
293 text
+= " -The Buildbot\n"
296 haveAttachments
= False
297 if patch
or self
.addLogs
:
298 haveAttachments
= True
299 if not canDoAttachments
:
300 twlog
.msg("warning: I want to send mail with attachments, "
301 "but this python is too old to have "
302 "email.MIMEMultipart . Please upgrade to python-2.3 "
303 "or newer to enable addLogs=True")
305 if haveAttachments
and canDoAttachments
:
307 m
.attach(MIMEText(text
))
312 m
['Date'] = formatdate(localtime
=True)
313 m
['Subject'] = self
.subject
% { 'result': res
,
314 'projectName': projectName
,
317 m
['From'] = self
.fromaddr
318 # m['To'] is added later
322 a
.add_header('Content-Disposition', "attachment",
323 filename
="source patch")
326 for log
in build
.getLogs():
327 name
= "%s.%s" % (log
.getStep().getName(),
329 a
= MIMEText(log
.getText())
330 a
.add_header('Content-Disposition', "attachment",
334 # now, who is this message going to?
336 recipients
= self
.extraRecipients
[:]
337 if self
.sendToInterestedUsers
and self
.lookup
:
338 for u
in build
.getInterestedUsers():
339 d
= defer
.maybeDeferred(self
.lookup
.getAddress
, u
)
340 d
.addCallback(recipients
.append
)
342 d
= defer
.DeferredList(dl
)
343 d
.addCallback(self
._gotRecipients
, recipients
, m
)
346 def _gotRecipients(self
, res
, rlist
, m
):
349 if r
is not None and r
not in recipients
:
352 m
['To'] = ", ".join(recipients
)
353 return self
.sendMessage(m
, recipients
)
355 def sendMessage(self
, m
, recipients
):
358 twlog
.msg("sending mail (%d bytes) to" % len(s
), recipients
)
359 for recip
in recipients
:
360 ds
.append(sendmail(self
.relayhost
, self
.fromaddr
, recip
, s
))
361 return defer
.DeferredList(ds
)