| Index: third_party/buildbot_7_12/buildbot/status/mail.py
|
| diff --git a/third_party/buildbot_7_12/buildbot/status/mail.py b/third_party/buildbot_7_12/buildbot/status/mail.py
|
| deleted file mode 100644
|
| index 30bc19d61c6e9f2e1626dec1a4f835d69eeba28c..0000000000000000000000000000000000000000
|
| --- a/third_party/buildbot_7_12/buildbot/status/mail.py
|
| +++ /dev/null
|
| @@ -1,517 +0,0 @@
|
| -# -*- test-case-name: buildbot.test.test_status -*-
|
| -
|
| -# the email.MIMEMultipart module is only available in python-2.2.2 and later
|
| -import re
|
| -
|
| -from email.Message import Message
|
| -from email.Utils import formatdate
|
| -from email.MIMEText import MIMEText
|
| -try:
|
| - from email.MIMEMultipart import MIMEMultipart
|
| - canDoAttachments = True
|
| -except ImportError:
|
| - canDoAttachments = False
|
| -import urllib
|
| -
|
| -from zope.interface import implements
|
| -from twisted.internet import defer
|
| -from twisted.mail.smtp import sendmail
|
| -from twisted.python import log as twlog
|
| -
|
| -from buildbot import interfaces, util
|
| -from buildbot.status import base
|
| -from buildbot.status.builder import FAILURE, SUCCESS, WARNINGS, Results
|
| -
|
| -import sys
|
| -if sys.version_info[:3] < (2,4,0):
|
| - from sets import Set as set
|
| -
|
| -VALID_EMAIL = re.compile("[a-zA-Z0-9\.\_\%\-\+]+@[a-zA-Z0-9\.\_\%\-]+.[a-zA-Z]{2,6}")
|
| -
|
| -class Domain(util.ComparableMixin):
|
| - implements(interfaces.IEmailLookup)
|
| - compare_attrs = ["domain"]
|
| -
|
| - def __init__(self, domain):
|
| - assert "@" not in domain
|
| - self.domain = domain
|
| -
|
| - def getAddress(self, name):
|
| - """If name is already an email address, pass it through."""
|
| - if '@' in name:
|
| - return name
|
| - return name + "@" + self.domain
|
| -
|
| -
|
| -class MailNotifier(base.StatusReceiverMultiService):
|
| - """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.
|
| - """
|
| -
|
| - implements(interfaces.IEmailSender)
|
| -
|
| - compare_attrs = ["extraRecipients", "lookup", "fromaddr", "mode",
|
| - "categories", "builders", "addLogs", "relayhost",
|
| - "subject", "sendToInterestedUsers", "customMesg",
|
| - "messageFormatter", "extraHeaders"]
|
| -
|
| - def __init__(self, fromaddr, mode="all", categories=None, builders=None,
|
| - addLogs=False, relayhost="localhost",
|
| - subject="buildbot %(result)s in %(projectName)s on %(builder)s",
|
| - lookup=None, extraRecipients=[],
|
| - sendToInterestedUsers=True, customMesg=None,
|
| - messageFormatter=None, extraHeaders=None, addPatch=True):
|
| - """
|
| - @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 string
|
| - @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.
|
| -
|
| - @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: string (defaults to all)
|
| - @param mode: one of:
|
| - - 'all': send mail about all builds, passing and failing
|
| - - 'failing': only send mail about builds which fail
|
| - - 'passing': only send mail about builds which succeed
|
| - - 'problem': only send mail about a build which failed
|
| - when the previous build passed
|
| - - 'change': only send mail about builds who change status
|
| -
|
| - @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 lookup: implementor of {IEmailLookup}
|
| - @param lookup: object which provides IEmailLookup, which is
|
| - responsible for mapping User names (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.
|
| - """
|
| -
|
| - base.StatusReceiverMultiService.__init__(self)
|
| - assert isinstance(extraRecipients, (list, tuple))
|
| - for r in extraRecipients:
|
| - assert isinstance(r, str)
|
| - assert VALID_EMAIL.search(r) # require full email addresses, not User names
|
| - self.extraRecipients = extraRecipients
|
| - self.sendToInterestedUsers = sendToInterestedUsers
|
| - self.fromaddr = fromaddr
|
| - assert mode in ('all', 'failing', 'problem', 'change', 'passing')
|
| - self.mode = mode
|
| - self.categories = categories
|
| - self.builders = builders
|
| - self.addLogs = addLogs
|
| - self.relayhost = relayhost
|
| - self.subject = subject
|
| - if lookup is not None:
|
| - if type(lookup) is str:
|
| - lookup = Domain(lookup)
|
| - assert interfaces.IEmailLookup.providedBy(lookup)
|
| - self.lookup = lookup
|
| - self.customMesg = customMesg
|
| - self.messageFormatter = messageFormatter
|
| - if extraHeaders:
|
| - assert isinstance(extraHeaders, dict)
|
| - self.extraHeaders = extraHeaders
|
| - self.addPatch = addPatch
|
| - self.watched = []
|
| - self.master_status = None
|
| -
|
| - # you should either limit on builders or categories, not both
|
| - if self.builders != None and self.categories != None:
|
| - twlog.err("Please specify only builders to ignore or categories to include")
|
| - raise # FIXME: the asserts above do not raise some Exception either
|
| -
|
| - if customMesg and messageFormatter:
|
| - twlog.err("Specify only one of customMesg and messageFormatter")
|
| - self.customMesg = None
|
| -
|
| - if customMesg:
|
| - twlog.msg("customMesg is deprecated; please use messageFormatter instead")
|
| -
|
| - def setServiceParent(self, parent):
|
| - """
|
| - @type parent: L{buildbot.master.BuildMaster}
|
| - """
|
| - base.StatusReceiverMultiService.setServiceParent(self, parent)
|
| - self.setup()
|
| -
|
| - def setup(self):
|
| - self.master_status = self.parent.getStatus()
|
| - self.master_status.subscribe(self)
|
| -
|
| - def disownServiceParent(self):
|
| - self.master_status.unsubscribe(self)
|
| - for w in self.watched:
|
| - w.unsubscribe(self)
|
| - return base.StatusReceiverMultiService.disownServiceParent(self)
|
| -
|
| - def builderAdded(self, name, builder):
|
| - # only subscribe to builders we are interested in
|
| - if self.categories != None and builder.category not in self.categories:
|
| - return None
|
| -
|
| - self.watched.append(builder)
|
| - return self # subscribe to this builder
|
| -
|
| - def builderRemoved(self, name):
|
| - pass
|
| -
|
| - def builderChangedState(self, name, state):
|
| - pass
|
| - def buildStarted(self, name, build):
|
| - pass
|
| - def buildFinished(self, name, build, results):
|
| - # here is where we actually do something.
|
| - builder = build.getBuilder()
|
| - if self.builders is not None and name not in self.builders:
|
| - return # ignore this build
|
| - if self.categories is not None and \
|
| - builder.category not in self.categories:
|
| - return # ignore this build
|
| -
|
| - if self.mode == "failing" and results != FAILURE:
|
| - return
|
| - if self.mode == "passing" and results != SUCCESS:
|
| - return
|
| - if self.mode == "problem":
|
| - if results != FAILURE:
|
| - return
|
| - prev = build.getPreviousBuild()
|
| - if prev and prev.getResults() == FAILURE:
|
| - return
|
| - if self.mode == "change":
|
| - prev = build.getPreviousBuild()
|
| - if not prev or prev.getResults() == results:
|
| - if prev:
|
| - print prev.getResults()
|
| - else:
|
| - print "no prev"
|
| - return
|
| - # 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.
|
| - return self.buildMessage(name, build, results)
|
| -
|
| - def getCustomMesgData(self, mode, name, build, results, master_status):
|
| - #
|
| - # logs is a list of tuples that contain the log
|
| - # name, log url, and the log contents as a list of strings.
|
| - #
|
| - logs = list()
|
| - for logf in build.getLogs():
|
| - 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))
|
| -
|
| - properties = build.getProperties()
|
| -
|
| - attrs = {'builderName': name,
|
| - 'projectName': master_status.getProjectName(),
|
| - 'mode': mode,
|
| - 'result': Results[results],
|
| - 'buildURL': master_status.getURLForThing(build),
|
| - 'buildbotURL': master_status.getBuildbotURL(),
|
| - 'buildText': build.getText(),
|
| - 'buildProperties': properties,
|
| - 'slavename': build.getSlavename(),
|
| - 'reason': build.getReason(),
|
| - 'responsibleUsers': build.getResponsibleUsers(),
|
| - 'branch': "",
|
| - 'revision': "",
|
| - 'patch': "",
|
| - 'changes': [],
|
| - 'logs': logs}
|
| -
|
| - ss = build.getSourceStamp()
|
| - if ss:
|
| - attrs['branch'] = ss.branch
|
| - attrs['revision'] = ss.revision
|
| - attrs['patch'] = ss.patch
|
| - attrs['changes'] = ss.changes[:]
|
| -
|
| - return attrs
|
| -
|
| - def defaultMessage(self, mode, name, build, results, master_status):
|
| - """Generate a buildbot mail message and return a tuple of message text
|
| - and type."""
|
| - result = Results[results]
|
| -
|
| - text = ""
|
| - if mode == "all":
|
| - text += "The Buildbot has finished a build"
|
| - elif mode == "failing":
|
| - text += "The Buildbot has detected a failed build"
|
| - elif mode == "passing":
|
| - text += "The Buildbot has detected a passing build"
|
| - elif mode == "change" and result == 'success':
|
| - text += "The Buildbot has detected a restored build"
|
| - else:
|
| - text += "The Buildbot has detected a new failure"
|
| - text += " of %s on %s.\n" % (name, master_status.getProjectName())
|
| - 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()
|
| -
|
| - source = ""
|
| - ss = build.getSourceStamp()
|
| - 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)"
|
| -
|
| - text += "Build Source Stamp: %s\n" % source
|
| -
|
| - text += "Blamelist: %s\n" % ",".join(build.getResponsibleUsers())
|
| -
|
| - text += "\n"
|
| -
|
| - t = build.getText()
|
| - if t:
|
| - t = ": " + " ".join(t)
|
| - else:
|
| - t = ""
|
| -
|
| - if result == 'success':
|
| - text += "Build succeeded!\n"
|
| - elif result == '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' }
|
| -
|
| - def buildMessage(self, name, build, results):
|
| - if self.customMesg:
|
| - # the customMesg stuff can be *huge*, so we prefer not to load it
|
| - attrs = self.getCustomMesgData(self.mode, name, build, results, self.master_status)
|
| - text, type = self.customMesg(attrs)
|
| - msgdict = { 'body' : text, 'type' : type }
|
| - elif self.messageFormatter:
|
| - msgdict = self.messageFormatter(self.mode, name, build, results, self.master_status)
|
| - else:
|
| - msgdict = self.defaultMessage(self.mode, name, build, results, self.master_status)
|
| -
|
| - text = msgdict['body']
|
| - type = msgdict['type']
|
| - if 'subject' in msgdict:
|
| - subject = msgdict['subject']
|
| - else:
|
| - subject = self.subject % { 'result': Results[results],
|
| - 'projectName': self.master_status.getProjectName(),
|
| - 'builder': name,
|
| - }
|
| -
|
| -
|
| - assert type in ('plain', 'html'), "'%s' message type must be 'plain' or 'html'." % type
|
| -
|
| - haveAttachments = False
|
| - ss = build.getSourceStamp()
|
| - if (ss and ss.patch and self.addPatch) or self.addLogs:
|
| - haveAttachments = True
|
| - if not canDoAttachments:
|
| - twlog.msg("warning: I want to send mail with attachments, "
|
| - "but this python is too old to have "
|
| - "email.MIMEMultipart . Please upgrade to python-2.3 "
|
| - "or newer to enable addLogs=True")
|
| -
|
| - if haveAttachments and canDoAttachments:
|
| - m = MIMEMultipart()
|
| - m.attach(MIMEText(text, type))
|
| - else:
|
| - m = Message()
|
| - m.set_payload(text)
|
| - m.set_type("text/%s" % type)
|
| -
|
| - m['Date'] = formatdate(localtime=True)
|
| - m['Subject'] = subject
|
| - m['From'] = self.fromaddr
|
| - # m['To'] is added later
|
| -
|
| - if ss and ss.patch and self.addPatch:
|
| - patch = ss.patch
|
| - a = MIMEText(patch[1])
|
| - a.add_header('Content-Disposition', "attachment",
|
| - filename="source patch")
|
| - m.attach(a)
|
| - if self.addLogs:
|
| - for log in build.getLogs():
|
| - name = "%s.%s" % (log.getStep().getName(),
|
| - log.getName())
|
| - if self._shouldAttachLog(log.getName()) or self._shouldAttachLog(name):
|
| - a = MIMEText(log.getText())
|
| - a.add_header('Content-Disposition', "attachment",
|
| - filename=name)
|
| - m.attach(a)
|
| -
|
| - # Add any extra headers that were requested, doing WithProperties
|
| - # interpolation if necessary
|
| - if self.extraHeaders:
|
| - for k,v in self.extraHeaders.items():
|
| - k = properties.render(k)
|
| - if k in m:
|
| - twlog("Warning: Got header " + k + " in self.extraHeaders "
|
| - "but it already exists in the Message - "
|
| - "not adding it.")
|
| - continue
|
| - m[k] = properties.render(v)
|
| -
|
| - # now, who is this message going to?
|
| - dl = []
|
| - recipients = []
|
| - if self.sendToInterestedUsers and self.lookup:
|
| - for u in build.getInterestedUsers():
|
| - d = defer.maybeDeferred(self.lookup.getAddress, u)
|
| - d.addCallback(recipients.append)
|
| - dl.append(d)
|
| - d = defer.DeferredList(dl)
|
| - d.addCallback(self._gotRecipients, recipients, m)
|
| - return d
|
| -
|
| - def _shouldAttachLog(self, logname):
|
| - if type(self.addLogs) is bool:
|
| - return self.addLogs
|
| - return logname in self.addLogs
|
| -
|
| - def _gotRecipients(self, res, rlist, m):
|
| - recipients = set()
|
| -
|
| - for r in rlist:
|
| - if r is None: # getAddress didn't like this address
|
| - continue
|
| -
|
| - # Git can give emails like 'User' <user@foo.com>@foo.com so check
|
| - # for two @ and chop the last
|
| - if r.count('@') > 1:
|
| - r = r[:r.rindex('@')]
|
| -
|
| - if VALID_EMAIL.search(r):
|
| - recipients.add(r)
|
| - else:
|
| - twlog.msg("INVALID EMAIL: %r" + r)
|
| -
|
| - # if we're sending to interested users move the extra's to the CC
|
| - # list so they can tell if they are also interested in the change
|
| - # unless there are no interested users
|
| - if self.sendToInterestedUsers and len(recipients):
|
| - extra_recips = self.extraRecipients[:]
|
| - extra_recips.sort()
|
| - m['CC'] = ", ".join(extra_recips)
|
| - else:
|
| - [recipients.add(r) for r in self.extraRecipients[:]]
|
| -
|
| - rlist = list(recipients)
|
| - rlist.sort()
|
| - m['To'] = ", ".join(rlist)
|
| -
|
| - # The extras weren't part of the TO list so add them now
|
| - if self.sendToInterestedUsers:
|
| - for r in self.extraRecipients:
|
| - recipients.add(r)
|
| -
|
| - return self.sendMessage(m, list(recipients))
|
| -
|
| - def sendMessage(self, m, recipients):
|
| - s = m.as_string()
|
| - twlog.msg("sending mail (%d bytes) to" % len(s), recipients)
|
| - return sendmail(self.relayhost, self.fromaddr, recipients, s)
|
|
|