diff options
Diffstat (limited to 'lib/python2.7/site-packages/buildbot-0.8.8-py2.7.egg/buildbot/status/mail.py')
-rw-r--r-- | lib/python2.7/site-packages/buildbot-0.8.8-py2.7.egg/buildbot/status/mail.py | 798 |
1 files changed, 0 insertions, 798 deletions
diff --git a/lib/python2.7/site-packages/buildbot-0.8.8-py2.7.egg/buildbot/status/mail.py b/lib/python2.7/site-packages/buildbot-0.8.8-py2.7.egg/buildbot/status/mail.py deleted file mode 100644 index ef48d8e2..00000000 --- a/lib/python2.7/site-packages/buildbot-0.8.8-py2.7.egg/buildbot/status/mail.py +++ /dev/null @@ -1,798 +0,0 @@ -# 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 - - -import re -import types -from email.Message import Message -from email.Utils import formatdate -from email.MIMEText import MIMEText -from email.MIMENonMultipart import MIMENonMultipart -from email.MIMEMultipart import MIMEMultipart -from StringIO import StringIO -import urllib - -from zope.interface import implements -from twisted.internet import defer, reactor -from twisted.python import log as twlog - -try: - from twisted.mail.smtp import ESMTPSenderFactory - ESMTPSenderFactory = ESMTPSenderFactory # for pyflakes -except ImportError: - ESMTPSenderFactory = None - -have_ssl = True -try: - from twisted.internet import ssl - from OpenSSL.SSL import SSLv3_METHOD -except ImportError: - have_ssl = False - -# this incantation teaches email to output utf-8 using 7- or 8-bit encoding, -# although it has no effect before python-2.7. -from email import Charset -Charset.add_charset('utf-8', Charset.SHORTEST, None, 'utf-8') - -from buildbot import interfaces, util, config -from buildbot.process.users import users -from buildbot.status import base -from buildbot.status.results import FAILURE, SUCCESS, WARNINGS, EXCEPTION, Results - -VALID_EMAIL = re.compile("[a-zA-Z0-9\.\_\%\-\+]+@[a-zA-Z0-9\.\_\%\-]+.[a-zA-Z]{2,6}") - -ENCODING = 'utf8' -LOG_ENCODING = 'utf-8' - -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 - - -def defaultMessage(mode, name, build, results, master_status): - """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' } - -def defaultGetPreviousBuild(current_build): - return current_build.getPreviousBuild() - -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"] - - possible_modes = ("change", "failing", "passing", "problem", "warnings", "exception") - - def __init__(self, fromaddr, mode=("failing", "passing", "warnings"), - 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, - previousBuildGetter=defaultGetPreviousBuild): - """ - @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. - - @type previousBuildGetter: func - @param previousBuildGetter: function taking a BuildStatus instance - returning a BuildStatus of the build - previous to the one passed in. This allows - to implement a relative ordering between - builds other than the default one, which is - chronological. - """ - base.StatusReceiverMultiService.__init__(self) - - if not isinstance(extraRecipients, (list, tuple)): - config.error("extraRecipients must be a list or tuple") - else: - for r in extraRecipients: - if not isinstance(r, str) or not VALID_EMAIL.search(r): - config.error( - "extra recipient %r is not a valid email" % (r,)) - self.extraRecipients = extraRecipients - self.sendToInterestedUsers = sendToInterestedUsers - self.fromaddr = fromaddr - if isinstance(mode, basestring): - if mode == "all": - mode = ("failing", "passing", "warnings", "exception") - elif mode == "warnings": - mode = ("failing", "warnings") - else: - mode = (mode,) - for m in mode: - if m not in self.possible_modes: - config.error( - "mode %s is not a valid mode" % (m,)) - self.mode = mode - self.categories = categories - self.builders = builders - self.addLogs = addLogs - self.relayhost = relayhost - if '\n' in subject: - config.error( - 'Newlines are not allowed in email subjects') - 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: - if not isinstance(extraHeaders, dict): - config.error("extraHeaders must be a dictionary") - self.extraHeaders = extraHeaders - self.addPatch = addPatch - self.useTls = useTls - self.smtpUser = smtpUser - self.smtpPassword = smtpPassword - self.smtpPort = smtpPort - self.buildSetSummary = buildSetSummary - self.buildSetSubscription = None - self.getPreviousBuild = previousBuildGetter - self.watched = [] - self.master_status = None - - # you should either limit on builders or categories, not both - if self.builders != None and self.categories != None: - config.error( - "Please specify only builders or categories to include - " + - "not both.") - - if customMesg: - config.error( - "customMesg is deprecated; use messageFormatter instead") - - def setServiceParent(self, parent): - """ - @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 - - def startService(self): - if self.buildSetSummary: - self.buildSetSubscription = \ - self.master.subscribeToBuildsetCompletions(self.buildsetFinished) - - base.StatusReceiverMultiService.startService(self) - - def stopService(self): - if self.buildSetSubscription is not None: - self.buildSetSubscription.unsubscribe() - self.buildSetSubscription = None - - return base.StatusReceiverMultiService.stopService(self) - - def disownServiceParent(self): - self.master_status.unsubscribe(self) - self.master_status = None - 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 isMailNeeded(self, build, results): - # here is where we actually do something. - builder = build.getBuilder() - if self.builders is not None and builder.name not in self.builders: - return False # ignore this build - if self.categories is not None and \ - builder.category not in self.categories: - return False # ignore this build - - prev = self.getPreviousBuild(build) - if "change" in self.mode: - if prev and prev.getResults() != results: - return True - if "failing" in self.mode and results == FAILURE: - return True - if "passing" in self.mode and results == SUCCESS: - return True - if "problem" in self.mode and results == FAILURE: - if prev and prev.getResults() != FAILURE: - return True - if "warnings" in self.mode and results == WARNINGS: - return True - if "exception" in self.mode and results == EXCEPTION: - return True - - return False - - def buildFinished(self, name, build, results): - if ( not self.buildSetSummary and - 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. - return self.buildMessage(name, [build], results) - return None - - def _gotBuilds(self, res, buildset): - builds = [] - for (builddictlist, builder) in res: - for builddict in builddictlist: - build = builder.getBuild(builddict['number']) - if build is not None and self.isMailNeeded(build, build.results): - builds.append(build) - - if builds: - self.buildMessage("Buildset Complete: " + buildset['reason'], builds, - buildset['results']) - - def _gotBuildRequests(self, breqs, buildset): - dl = [] - for breq in breqs: - buildername = breq['buildername'] - builder = self.master_status.getBuilder(buildername) - d = self.master.db.builds.getBuildsForRequest(breq['brid']) - d.addCallback(lambda builddictlist, builder=builder: - (builddictlist, builder)) - dl.append(d) - d = defer.gatherResults(dl) - d.addCallback(self._gotBuilds, buildset) - - def _gotBuildSet(self, buildset, bsid): - d = self.master.db.buildrequests.getBuildRequests(bsid=bsid) - d.addCallback(self._gotBuildRequests, buildset) - - def buildsetFinished(self, bsid, result): - d = self.master.db.buildsets.getBuildset(bsid=bsid) - d.addCallback(self._gotBuildSet, bsid) - - return d - - 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)) - - attrs = {'builderName': name, - '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} - - ss = None - ss_list = build.getSourceStamps() - - if ss_list: - if len(ss_list) == 1: - ss = ss_list[0] - if ss: - attrs['branch'] = ss.branch - attrs['revision'] = ss.revision - attrs['patch'] = ss.patch - attrs['patch_info'] = ss.patch_info - attrs['changes'] = ss.changes[:] - else: - for key in ['branch', 'revision', 'patch', 'patch_info', 'changes']: - attrs[key] = {} - for ss in ss_list: - attrs['branch'][ss.codebase] = ss.branch - attrs['revision'][ss.codebase] = ss.revision - attrs['patch'][ss.codebase] = ss.patch - attrs['patch_info'][ss.codebase] = ss.patch_info - attrs['changes'][ss.codebase] = ss.changes[:] - - return attrs - - def patch_to_attachment(self, patch, index): - # 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. - if type(patch[1]) != types.UnicodeType: - try: - unicode = patch[1].decode('utf8') - except UnicodeDecodeError: - unicode = None - else: - unicode = patch[1] - - if unicode: - a = MIMEText(unicode.encode(ENCODING), _charset=ENCODING) - else: - # MIMEApplication is not present in Python-2.4 :( - a = MIMENonMultipart('application', 'octet-stream') - a.set_payload(patch[1]) - a.add_header('Content-Disposition', "attachment", - filename="source patch " + str(index) ) - return a - - def createEmail(self, msgdict, builderName, title, results, builds=None, - patches=None, logs=None): - text = msgdict['body'].encode(ENCODING) - type = msgdict['type'] - if 'subject' in msgdict: - subject = msgdict['subject'].encode(ENCODING) - else: - subject = self.subject % { 'result': Results[results], - 'projectName': title, - 'title': title, - 'builder': builderName, - } - - assert '\n' not in subject, \ - "Subject cannot contain newlines" - - assert type in ('plain', 'html'), \ - "'%s' message type must be 'plain' or 'html'." % type - - if patches or logs: - m = MIMEMultipart() - m.attach(MIMEText(text, type, ENCODING)) - else: - m = Message() - m.set_payload(text, ENCODING) - m.set_type("text/%s" % type) - - m['Date'] = formatdate(localtime=True) - m['Subject'] = subject - m['From'] = self.fromaddr - # m['To'] is added later - - if patches: - for (i, patch) in enumerate(patches): - a = self.patch_to_attachment(patch, i) - m.attach(a) - if logs: - for log in logs: - name = "%s.%s" % (log.getStep().getName(), - log.getName()) - if ( self._shouldAttachLog(log.getName()) or - self._shouldAttachLog(name) ): - text = log.getText() - if not isinstance(text, unicode): - # guess at the encoding, and use replacement symbols - # for anything that's not in that encoding - text = text.decode(LOG_ENCODING, 'replace') - a = MIMEText(text.encode(ENCODING), - _charset=ENCODING) - a.add_header('Content-Disposition', "attachment", - filename=name) - m.attach(a) - - #@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 - if self.extraHeaders: - if len(builds) == 1: - d = builds[0].render(self.extraHeaders) - else: - d = defer.succeed(self.extraHeaders) - @d.addCallback - def addExtraHeaders(extraHeaders): - for k,v in extraHeaders.items(): - if k in m: - twlog.msg("Warning: Got header " + k + - " in self.extraHeaders " - "but it already exists in the Message - " - "not adding it.") - m[k] = v - d.addCallback(lambda _: m) - return d - - return defer.succeed(m) - - def buildMessageDict(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 } - else: - msgdict = self.messageFormatter(self.mode, name, build, results, - self.master_status) - - return msgdict - - - def buildMessage(self, name, builds, results): - patches = [] - logs = [] - msgdict = {"body":""} - - for build in builds: - ss_list = build.getSourceStamps() - if self.addPatch: - for ss in ss_list: - if ss.patch: - patches.append(ss.patch) - if self.addLogs: - logs.extend(build.getLogs()) - - tmp = self.buildMessageDict(name=build.getBuilder().name, - build=build, results=build.results) - msgdict['body'] += tmp['body'] - msgdict['body'] += '\n\n' - msgdict['type'] = tmp['type'] - if "subject" in tmp: - msgdict['subject'] = tmp['subject'] - - d = self.createEmail(msgdict, name, self.master_status.getTitle(), - results, builds, patches, logs) - - @d.addCallback - def getRecipients(m): - # now, who is this message going to? - if self.sendToInterestedUsers: - dl = [] - for build in builds: - if self.lookup: - d = self.useLookup(build) - else: - d = self.useUsers(build) - dl.append(d) - d = defer.gatherResults(dl) - else: - d = defer.succeed([]) - d.addCallback(self._gotRecipients, m) - return d - - def useLookup(self, build): - dl = [] - for u in build.getResponsibleUsers() + build.getInterestedUsers(): - d = defer.maybeDeferred(self.lookup.getAddress, u) - dl.append(d) - return defer.gatherResults(dl) - - def useUsers(self, build): - return users.getBuildContacts(self.master, build, ['email']) - - def _shouldAttachLog(self, logname): - if type(self.addLogs) is bool: - return self.addLogs - return logname in self.addLogs - - def _gotRecipients(self, rlist, m): - to_recipients = set() - cc_recipients = set() - - for r in reduce(list.__add__, 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): - to_recipients.add(r) - else: - twlog.msg("INVALID EMAIL: %r" + r) - - # 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: - if self.sendToInterestedUsers and to_recipients: - cc_recipients.update(self.extraRecipients) - else: - to_recipients.update(self.extraRecipients) - - m['To'] = ", ".join(sorted(to_recipients)) - if cc_recipients: - m['CC'] = ", ".join(sorted(cc_recipients)) - - return self.sendMessage(m, list(to_recipients | cc_recipients)) - - def sendmail(self, s, recipients): - 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 - - def sendMessage(self, m, recipients): - s = m.as_string() - twlog.msg("sending mail (%d bytes) to" % len(s), recipients) - return self.sendmail(s, recipients) - |