Next: , Previous: WebStatus, Up: Status Targets


4.13.2 MailNotifier

The buildbot can also send email when builds finish. The most common use of this is to tell developers when their change has caused the build to fail. It is also quite common to send a message to a mailing list (usually named “builds” or similar) about every build.

The MailNotifier status target is used to accomplish this. You configure it by specifying who mail should be sent to, under what circumstances mail should be sent, and how to deliver the mail. It can be configured to only send out mail for certain builders, and only send messages when the build fails, or when the builder 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 (see Doing Things With Users), which includes all developers who made changes in the build. You can add additional recipients with the extraRecipients argument. You can also add interested users by setting the owners build property to a list of users in the scheduler constructor (see Configuring Schedulers).

Each MailNotifier sends mail to a single set of recipients. To send different kinds of mail to different recipients, use multiple MailNotifiers.

The following simple example will send an email upon the completion of each build, to just those developers whose Changes were included in the build. The email contains a description of the Build, its results, and URLs where more information can be obtained.

     from buildbot.status.mail import MailNotifier
     mn = MailNotifier(fromaddr="buildbot@example.org", lookup="example.org")
     c['status'].append(mn)

To get a simple one-message-per-build (say, for a mailing list), use the following form instead. This form does not send mail to individual developers (and thus does not need the lookup= argument, explained below), instead it only ever sends mail to the “extra recipients” named in the arguments:

     mn = MailNotifier(fromaddr="buildbot@example.org",
                       sendToInterestedUsers=False,
                       extraRecipients=['listaddr@example.org'])

If your SMTP host requires authentication before it allows you to send emails, this can also be done by specifying “smtpUser” and “smptPassword”:

     mn = MailNotifier(fromaddr="myuser@gmail.com",
                       sendToInterestedUsers=False,
                       extraRecipients=["listaddr@example.org"],
                       relayhost="smtp.gmail.com", smtpPort=587,
                       smtpUser="myuser@gmail.com", smtpPassword="mypassword")

If you want to require Transport Layer Security (TLS), then you can also set “useTls”:

     mn = MailNotifier(fromaddr="myuser@gmail.com",
                       sendToInterestedUsers=False,
                       extraRecipients=["listaddr@example.org"],
                       useTls=True, relayhost="smtp.gmail.com", smtpPort=587,
                       smtpUser="myuser@gmail.com", smtpPassword="mypassword")

In some cases it is desirable to have different information then what is provided in a standard MailNotifier message. For this purpose MailNotifier provides the argument messageFormatter (a function) which allows for the creation of messages with unique content.

For example, if only short emails are desired (e.g., for delivery to phones)

     from buildbot.status.builder import Results
     def messageFormatter(mode, name, build, results, master_status):
         result = Results[results]
     
         text = list()
         text.append("STATUS: %s" % result.title())
         return {
             'body' : "\n".join(text),
             'type' : 'plain'
         }
     
     mn = MailNotifier(fromaddr="buildbot@example.org",
                       sendToInterestedUsers=False,
                       mode='problem',
                       extraRecipients=['listaddr@example.org'],
                       messageFormatter=messageFormatter)

Another example of a function delivering a customized html email containing the last 80 log lines of the last build step is given below:

     from buildbot.status.builder import Results
     
     def html_message_formatter(mode, name, build, results, master_status):
         """Provide a customized message to BuildBot's MailNotifier.
     
         The last 80 lines of the log are provided as well as the changes
         relevant to the build.  Message content is formatted as html.
         """
         result = Results[results]
     
         limit_lines = 80
         text = list()
         text.append(u'<h4>Build status: %s</h4>' % result.upper())
         text.append(u'<table cellspacing="10"><tr>')
         text.append(u"<td>Buildslave for this Build:</td><td><b>%s</b></td></tr>" % build.getSlavename())
         if master_status.getURLForThing(build):
             text.append(u'<tr><td>Complete logs for all build steps:</td><td><a href="%s">%s</a></td></tr>'
                         % (master_status.getURLForThing(build),
                            master_status.getURLForThing(build))
                         )
             text.append(u'<tr><td>Build Reason:</td><td>%s</td></tr>' % build.getReason())
             source = u""
             ss = build.getSourceStamp()
             if ss.branch:
                 source += u"[branch %s] " % ss.branch
             if ss.revision:
                 source +=  ss.revision
             else:
                 source += u"HEAD"
             if ss.patch:
                 source += u" (plus patch)"
             text.append(u"<tr><td>Build Source Stamp:</td><td><b>%s</b></td></tr>" % source)
             text.append(u"<tr><td>Blamelist:</td><td>%s</td></tr>" % ",".join(build.getResponsibleUsers()))
             text.append(u'</table>')
             if ss.changes:
                 text.append(u'<h4>Recent Changes:</h4>')
                 for c in ss.changes:
                     cd = c.asDict()
                     when = datetime.datetime.fromtimestamp(cd['when'] ).ctime()
                     text.append(u'<table cellspacing="10">')
                     text.append(u'<tr><td>Repository:</td><td>%s</td></tr>' % cd['repository'] )
                     text.append(u'<tr><td>Project:</td><td>%s</td></tr>' % cd['project'] )
                     text.append(u'<tr><td>Time:</td><td>%s</td></tr>' % when)
                     text.append(u'<tr><td>Changed by:</td><td>%s</td></tr>' % cd['who'] )
                     text.append(u'<tr><td>Comments:</td><td>%s</td></tr>' % cd['comments'] )
                     text.append(u'</table>')
                     files = cd['files']
                     if files:
                         text.append(u'<table cellspacing="10"><tr><th align="left">Files</th><th>URL</th></tr>')
                         for file in files:
                             text.append(u'<tr><td>%s:</td><td>%s</td></tr>' % (file['name'], file['url']))
                         text.append(u'</table>')
             text.append(u'<br>')
             # get log for last step
             logs = build.getLogs()
             # logs within a step are in reverse order. Search back until we find stdio
             for log in reversed(logs):
                 if log.getName() == 'stdio':
                     break
             name = "%s.%s" % (log.getStep().getName(), log.getName())
             status, dummy = log.getStep().getResults()
             content = log.getText().splitlines() # Note: can be VERY LARGE
             url = u'%s/steps/%s/logs/%s' % (master_status.getURLForThing(build),
                                            log.getStep().getName(),
                                            log.getName())
     
             text.append(u'<i>Detailed log of last build step:</i> <a href="%s">%s</a>'
                         % (url, url))
             text.append(u'<br>')
             text.append(u'<h4>Last %d lines of "%s"</h4>' % (limit_lines, name))
             unilist = list()
             for line in content[len(content)-limit_lines:]:
                 unilist.append(cgi.escape(unicode(line,'utf-8')))
             text.append(u'<pre>'.join([uniline for uniline in unilist]))
             text.append(u'</pre>')
             text.append(u'<br><br>')
             text.append(u'<b>-The BuildBot</b>')
             return {
                 'body': u"\n".join(text),
                 'type': 'html'
                 }
     
     mn = MailNotifier(fromaddr="buildbot@example.org",
                       sendToInterestedUsers=False,
                       mode='failing',
                       extraRecipients=['listaddr@example.org'],
                       messageFormatter=message_formatter)

MailNotifier arguments

fromaddr
The email address to be used in the 'From' header.
sendToInterestedUsers
(boolean). If True (the default), send mail to all of the Interested Users. If False, only send mail to the extraRecipients list.
extraRecipients
(list of strings). 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.
subject
(string). 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.
mode
(string). Default to 'all'. One of:
all
Send mail about all builds, both passing and failing
change
Only send mail about builds which change status
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 has passed. If your builds usually pass, then this will only send mail when a problem occurs.

builders
(list of strings). 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.
categories
(list of strings). A list of category names to serve status information for. Defaults to None (all categories). Use either builders or categories, but not both.
addLogs
(boolean). 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.
addPatch
(boolean). If True, include the patch content if a patch was present. Patches are usually used on a Try server. Defaults to True.
relayhost
(string). The host to which the outbound SMTP connection should be made. Defaults to 'localhost'
smtpPort
(int). The port that will be used on outbound SMTP connections. Defaults to 25.
useTls
(boolean). When this argument is True (default is False) MailNotifier sends emails using TLS and authenticates with the relayhost. When using TLS the arguments smtpUser and smtpPassword must also be specified.
smtpUser
(string). The user name to use when authenticating with the relayhost.
smtpPassword
(string). The password that will be used when authenticating with the relayhost.
lookup
(implementor of IEmailLookup). 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. See buildbot/status/mail.py for more details.
messageFormatter
This is a optional function that can be used to generate a custom mail message. A messageFormatter function takes the mail mode (mode), builder name (name), the build status (build), the result code (results), and the BuildMaster status (master_status). It returns a dictionary. The body key gives a string that is 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 subject key is optional, but gives the subject for the email.
extraHeaders
(dictionary) A dictionary containing key/value pairs of extra headers to add to sent e-mails. Both the keys and the values may be a WithProperties instance.

As a help to those writing messageFormatter functions, the following table describes how to get some useful pieces of information from the various status objects:

•Name of the builder that generated this event
name
•Name of the project
master_status.getProjectName()
•MailNotifier mode
mode (one of all, failing, problem, change, passing)
•Builder result as a string
          from buildbot.status.builder import Results
          result_str = Results[results]
          # one of 'success', 'warnings', 'failure', 'skipped', or 'exception'

•URL to build page
master_status.getURLForThing(build)
•URL to buildbot main page.
master_status.getBuildbotURL()
•Build text
build.getText()
•Mapping of property names to values
build.getProperties() (a Properties instance)
•Slave name
build.getSlavename()
•Build reason (from a forced build)
build.getReason()
•List of responsible users
build.getResponsibleUsers()
•Source information (only valid if ss is not None)
          ss = build.getSourceStamp()
          if ss:
              branch = ss.branch
              revision = ss.revision
              patch = ss.patch
              changes = ss.changes # list

A change object has the following useful information:

who
(str) who made this change
revision
(str) what VC revision is this change
branch
(str) on what branch did this change occur
when
(str) when did this change occur
files
(list of str) what files were affected in this change
comments
(str) comments reguarding the change.
The Change methods asText and asDict can be used to format the information above. asText returns a list of strings and asDict returns a dictonary suitable for html/mail rendering.
•Log information
          logs = list()
          for log in build.getLogs():
              log_name = "%s.%s" % (log.getStep().getName(), log.getName())
              log_status, dummy = log.getStep().getResults()
              log_body = log.getText().splitlines() # Note: can be VERY LARGE
              log_url = '%s/steps/%s/logs/%s' % (master_status.getURLForThing(build),
                                                 log.getStep().getName(),
                                                 log.getName())
              logs.append((log_name, log_url, log_body, log_status))