|
# This file is part of Buildbot. Buildbot is free software: you can # redistribute it and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation, version 2. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Copyright Buildbot Team Members
except ImportError: ESMTPSenderFactory = None
from OpenSSL.SSL import SSLv3_METHOD
# this incantation teaches email to output utf-8 using 7- or 8-bit encoding, # although it has no effect before python-2.7.
"""If name is already an email address, pass it through."""
"""Generate a buildbot mail message and return a tuple of message text and type.""" ss_list = build.getSourceStamps()
prev = build.getPreviousBuild()
text = "" if results == FAILURE: if "change" in mode and prev and prev.getResults() != results or \ "problem" in mode and prev and prev.getResults() != FAILURE: text += "The Buildbot has detected a new failure" else: text += "The Buildbot has detected a failed build" elif results == WARNINGS: text += "The Buildbot has detected a problem in the build" elif results == SUCCESS: if "change" in mode and prev and prev.getResults() != results: text += "The Buildbot has detected a restored build" else: text += "The Buildbot has detected a passing build" elif results == EXCEPTION: text += "The Buildbot has detected a build exception"
projects = [] if ss_list: for ss in ss_list: if ss.project and ss.project not in projects: projects.append(ss.project) if not projects: projects = [master_status.getTitle()] text += " on builder %s while building %s.\n" % (name, ', '.join(projects))
if master_status.getURLForThing(build): text += "Full details are available at:\n %s\n" % master_status.getURLForThing(build) text += "\n"
if master_status.getBuildbotURL(): text += "Buildbot URL: %s\n\n" % urllib.quote(master_status.getBuildbotURL(), '/:')
text += "Buildslave for this Build: %s\n\n" % build.getSlavename() text += "Build Reason: %s\n" % build.getReason()
for ss in ss_list: source = "" if ss and ss.branch: source += "[branch %s] " % ss.branch if ss and ss.revision: source += str(ss.revision) else: source += "HEAD" if ss and ss.patch: source += " (plus patch)"
discriminator = "" if ss.codebase: discriminator = " '%s'" % ss.codebase text += "Build Source Stamp%s: %s\n" % (discriminator, source)
text += "Blamelist: %s\n" % ",".join(build.getResponsibleUsers())
text += "\n"
t = build.getText() if t: t = ": " + " ".join(t) else: t = ""
if results == SUCCESS: text += "Build succeeded!\n" elif results == WARNINGS: text += "Build Had Warnings%s\n" % t else: text += "BUILD FAILED%s\n" % t
text += "\n" text += "sincerely,\n" text += " -The Buildbot\n" text += "\n" return { 'body' : text, 'type' : 'plain' }
"""This is a status notifier which sends email to a list of recipients upon the completion of each build. It can be configured to only send out mail for certain builds, and only send messages when the build fails, or when it transitions from success to failure. It can also be configured to include various build logs in each message.
By default, the message will be sent to the Interested Users list, which includes all developers who made changes in the build. You can add additional recipients with the extraRecipients argument.
To get a simple one-message-per-build (say, for a mailing list), use sendToInterestedUsers=False, extraRecipients=['listaddr@example.org']
Each MailNotifier sends mail to a single set of recipients. To send different kinds of mail to different recipients, use multiple MailNotifiers. """
"categories", "builders", "addLogs", "relayhost", "subject", "sendToInterestedUsers", "customMesg", "messageFormatter", "extraHeaders"]
categories=None, builders=None, addLogs=False, relayhost="localhost", buildSetSummary=False, subject="buildbot %(result)s in %(title)s on %(builder)s", lookup=None, extraRecipients=[], sendToInterestedUsers=True, customMesg=None, messageFormatter=defaultMessage, extraHeaders=None, addPatch=True, useTls=False, smtpUser=None, smtpPassword=None, smtpPort=25): """ @type fromaddr: string @param fromaddr: the email address to be used in the 'From' header. @type sendToInterestedUsers: boolean @param sendToInterestedUsers: if True (the default), send mail to all of the Interested Users. If False, only send mail to the extraRecipients list.
@type extraRecipients: tuple of strings @param extraRecipients: a list of email addresses to which messages should be sent (in addition to the InterestedUsers list, which includes any developers who made Changes that went into this build). It is a good idea to create a small mailing list and deliver to that, then let subscribers come and go as they please. The addresses in this list are used literally (they are not processed by lookup).
@type subject: string @param subject: a string to be used as the subject line of the message. %(builder)s will be replaced with the name of the builder which provoked the message.
@type mode: list of strings @param mode: a list of MailNotifer.possible_modes: - "change": send mail about builds which change status - "failing": send mail about builds which fail - "passing": send mail about builds which succeed - "problem": send mail about a build which failed when the previous build passed - "warnings": send mail if a build contain warnings - "exception": send mail if a build fails due to an exception - "all": always send mail Defaults to ("failing", "passing", "warnings").
@type builders: list of strings @param builders: a list of builder names for which mail should be sent. Defaults to None (send mail for all builds). Use either builders or categories, but not both.
@type categories: list of strings @param categories: a list of category names to serve status information for. Defaults to None (all categories). Use either builders or categories, but not both.
@type addLogs: boolean @param addLogs: if True, include all build logs as attachments to the messages. These can be quite large. This can also be set to a list of log names, to send a subset of the logs. Defaults to False.
@type addPatch: boolean @param addPatch: if True, include the patch when the source stamp includes one.
@type relayhost: string @param relayhost: the host to which the outbound SMTP connection should be made. Defaults to 'localhost'
@type buildSetSummary: boolean @param buildSetSummary: if True, this notifier will only send a summary email when a buildset containing any of its watched builds completes
@type lookup: implementor of {IEmailLookup} @param lookup: object which provides IEmailLookup, which is responsible for mapping User names for Interested Users (which come from the VC system) into valid email addresses. If not provided, the notifier will only be able to send mail to the addresses in the extraRecipients list. Most of the time you can use a simple Domain instance. As a shortcut, you can pass as string: this will be treated as if you had provided Domain(str). For example, lookup='twistedmatrix.com' will allow mail to be sent to all developers whose SVN usernames match their twistedmatrix.com account names.
@type customMesg: func @param customMesg: (this function is deprecated)
@type messageFormatter: func @param messageFormatter: function taking (mode, name, build, result, master_status) and returning a dictionary containing two required keys "body" and "type", with a third optional key, "subject". The "body" key gives a string that contains the complete text of the message. The "type" key is the message type ('plain' or 'html'). The 'html' type should be used when generating an HTML message. The optional "subject" key gives the subject for the email.
@type extraHeaders: dict @param extraHeaders: A dict of extra headers to add to the mail. It's best to avoid putting 'To', 'From', 'Date', 'Subject', or 'CC' in here. Both the names and values may be WithProperties instances.
@type useTls: boolean @param useTls: Send emails using TLS and authenticate with the smtp host. Defaults to False.
@type smtpUser: string @param smtpUser: The user that will attempt to authenticate with the relayhost when useTls is True.
@type smtpPassword: string @param smtpPassword: The password that smtpUser will use when authenticating with relayhost.
@type smtpPort: int @param smtpPort: The port that will be used when connecting to the relayhost. Defaults to 25. """
config.error("extraRecipients must be a list or tuple") else: config.error( "extra recipient %r is not a valid email" % (r,)) else: config.error( "mode %s is not a valid mode" % (m,)) config.error( 'Newlines are not allowed in email subjects') config.error("extraHeaders must be a dictionary")
# you should either limit on builders or categories, not both "Please specify only builders or categories to include - " + "not both.")
config.error( "customMesg is deprecated; use messageFormatter instead")
""" @type parent: L{buildbot.master.BuildMaster} """ base.StatusReceiverMultiService.setServiceParent(self, parent) self.master_status = self.parent self.master_status.subscribe(self) self.master = self.master_status.master
if self.buildSetSummary: self.buildSetSubscription = \ self.master.subscribeToBuildsetCompletions(self.buildsetFinished)
base.StatusReceiverMultiService.startService(self)
if self.buildSetSubscription is not None: self.buildSetSubscription.unsubscribe() self.buildSetSubscription = None
return base.StatusReceiverMultiService.stopService(self)
self.master_status.unsubscribe(self) self.master_status = None for w in self.watched: w.unsubscribe(self) return base.StatusReceiverMultiService.disownServiceParent(self)
# only subscribe to builders we are interested in
pass
pass
pass
# here is where we actually do something. builder.category not in self.categories:
return True return True
self.isMailNeeded(build, results) ): # for testing purposes, buildMessage returns a Deferred that fires # when the mail has been sent. To help unit tests, we return that # Deferred here even though the normal IStatusReceiver.buildFinished # signature doesn't do anything with it. If that changes (if # .buildFinished's return value becomes significant), we need to # rearrange this.
buildset['results'])
# # logs is a list of tuples that contain the log # name, log url, and the log contents as a list of strings. # logStep = logf.getStep() stepName = logStep.getName() logStatus, dummy = logStep.getResults() logName = logf.getName() logs.append(('%s.%s' % (stepName, logName), '%s/steps/%s/logs/%s' % ( master_status.getURLForThing(build), stepName, logName), logf.getText().splitlines(), logStatus))
'title': master_status.getTitle(), 'mode': mode, 'result': Results[results], 'buildURL': master_status.getURLForThing(build), 'buildbotURL': master_status.getBuildbotURL(), 'buildText': build.getText(), 'buildProperties': build.getProperties(), 'slavename': build.getSlavename(), 'reason': build.getReason().replace('\n', ''), 'responsibleUsers': build.getResponsibleUsers(), 'branch': "", 'revision': "", 'patch': "", 'patch_info': "", 'changes': [], 'logs': logs}
else:
# patches don't come with an encoding. If the patch is valid utf-8, # we'll attach it as MIMEText; otherwise, it gets attached as a binary # file. This will suit the vast majority of cases, since utf8 is by # far the most common encoding. else:
else: # MIMEApplication is not present in Python-2.4 :( filename="source patch " + str(index) )
patches=None, logs=None): subject = msgdict['subject'].encode(ENCODING) else: 'projectName': title, 'title': title, 'builder': builderName, }
"Subject cannot contain newlines"
"'%s' message type must be 'plain' or 'html'." % type
else:
# m['To'] is added later
log.getName()) self._shouldAttachLog(name) ): _charset=ENCODING) filename=name)
#@todo: is there a better way to do this? # Add any extra headers that were requested, doing WithProperties # interpolation if only one build was given else: def addExtraHeaders(extraHeaders): twlog.msg("Warning: Got header " + k + " in self.extraHeaders " "but it already exists in the Message - " "not adding it.")
# the customMesg stuff can be *huge*, so we prefer not to load it self.master_status) else: msgdict = self.messageFormatter(self.mode, name, build, results, self.master_status)
patches.append(ss.patch)
build=build, results=build.results)
results, builds, patches, logs)
def getRecipients(m): # now, who is this message going to? else: else:
contact_type='email', uid=uid) twlog.msg("Unable to find email for uid: %r" % uid) if e not in build.getResponsibleUsers()] def gatherRecipients(res):
return logname in self.addLogs
continue
# Git can give emails like 'User' <user@foo.com>@foo.com so check # for two @ and chop the last r = r[:r.rindex('@')]
else:
# If we're sending to interested users put the extras in the # CC list so they can tell if they are also interested in the # change: else:
result = defer.Deferred()
if have_ssl and self.useTls: client_factory = ssl.ClientContextFactory() client_factory.method = SSLv3_METHOD else: client_factory = None
if self.smtpUser and self.smtpPassword: useAuth = True else: useAuth = False
if not ESMTPSenderFactory: raise RuntimeError("twisted-mail is not installed - cannot " "send mail") sender_factory = ESMTPSenderFactory( self.smtpUser, self.smtpPassword, self.fromaddr, recipients, StringIO(s), result, contextFactory=client_factory, requireTransportSecurity=self.useTls, requireAuthentication=useAuth)
reactor.connectTCP(self.relayhost, self.smtpPort, sender_factory)
return result
s = m.as_string() twlog.msg("sending mail (%d bytes) to" % len(s), recipients) return self.sendmail(s, recipients)
|