| Trees | Indices | Help |
|
|---|
|
|
1 # This file is part of Buildbot. Buildbot is free software: you can
2 # redistribute it and/or modify it under the terms of the GNU General Public
3 # License as published by the Free Software Foundation, version 2.
4 #
5 # This program is distributed in the hope that it will be useful, but WITHOUT
6 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
7 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
8 # details.
9 #
10 # You should have received a copy of the GNU General Public License along with
11 # this program; if not, write to the Free Software Foundation, Inc., 51
12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
13 #
14 # Copyright Buildbot Team Members
15
16
17 import re
18 import types
19 from email.Message import Message
20 from email.Utils import formatdate
21 from email.MIMEText import MIMEText
22 from email.MIMENonMultipart import MIMENonMultipart
23 from email.MIMEMultipart import MIMEMultipart
24 from StringIO import StringIO
25 import urllib
26
27 from zope.interface import implements
28 from twisted.internet import defer, reactor
29 from twisted.python import log as twlog
30
31 try:
32 from twisted.mail.smtp import ESMTPSenderFactory
33 ESMTPSenderFactory = ESMTPSenderFactory # for pyflakes
34 except ImportError:
35 ESMTPSenderFactory = None
36
37 have_ssl = True
38 try:
39 from twisted.internet import ssl
40 from OpenSSL.SSL import SSLv3_METHOD
41 except ImportError:
42 have_ssl = False
43
44 # this incantation teaches email to output utf-8 using 7- or 8-bit encoding,
45 # although it has no effect before python-2.7.
46 from email import Charset
47 Charset.add_charset('utf-8', Charset.SHORTEST, None, 'utf-8')
48
49 from buildbot import interfaces, util, config
50 from buildbot.process.users import users
51 from buildbot.status import base
52 from buildbot.status.results import FAILURE, SUCCESS, WARNINGS, EXCEPTION, Results
53
54 VALID_EMAIL = re.compile("[a-zA-Z0-9\.\_\%\-\+]+@[a-zA-Z0-9\.\_\%\-]+.[a-zA-Z]{2,6}")
55
56 ENCODING = 'utf8'
57 LOG_ENCODING = 'utf-8'
72
75 """Generate a buildbot mail message and return a tuple of message text
76 and type."""
77 ss_list = build.getSourceStamps()
78
79 prev = build.getPreviousBuild()
80
81 text = ""
82 if results == FAILURE:
83 if "change" in mode and prev and prev.getResults() != results or \
84 "problem" in mode and prev and prev.getResults() != FAILURE:
85 text += "The Buildbot has detected a new failure"
86 else:
87 text += "The Buildbot has detected a failed build"
88 elif results == WARNINGS:
89 text += "The Buildbot has detected a problem in the build"
90 elif results == SUCCESS:
91 if "change" in mode and prev and prev.getResults() != results:
92 text += "The Buildbot has detected a restored build"
93 else:
94 text += "The Buildbot has detected a passing build"
95 elif results == EXCEPTION:
96 text += "The Buildbot has detected a build exception"
97
98 projects = []
99 if ss_list:
100 for ss in ss_list:
101 if ss.project and ss.project not in projects:
102 projects.append(ss.project)
103 if not projects:
104 projects = [master_status.getTitle()]
105 text += " on builder %s while building %s.\n" % (name, ', '.join(projects))
106
107 if master_status.getURLForThing(build):
108 text += "Full details are available at:\n %s\n" % master_status.getURLForThing(build)
109 text += "\n"
110
111 if master_status.getBuildbotURL():
112 text += "Buildbot URL: %s\n\n" % urllib.quote(master_status.getBuildbotURL(), '/:')
113
114 text += "Buildslave for this Build: %s\n\n" % build.getSlavename()
115 text += "Build Reason: %s\n" % build.getReason()
116
117 for ss in ss_list:
118 source = ""
119 if ss and ss.branch:
120 source += "[branch %s] " % ss.branch
121 if ss and ss.revision:
122 source += str(ss.revision)
123 else:
124 source += "HEAD"
125 if ss and ss.patch:
126 source += " (plus patch)"
127
128 discriminator = ""
129 if ss.codebase:
130 discriminator = " '%s'" % ss.codebase
131 text += "Build Source Stamp%s: %s\n" % (discriminator, source)
132
133 text += "Blamelist: %s\n" % ",".join(build.getResponsibleUsers())
134
135 text += "\n"
136
137 t = build.getText()
138 if t:
139 t = ": " + " ".join(t)
140 else:
141 t = ""
142
143 if results == SUCCESS:
144 text += "Build succeeded!\n"
145 elif results == WARNINGS:
146 text += "Build Had Warnings%s\n" % t
147 else:
148 text += "BUILD FAILED%s\n" % t
149
150 text += "\n"
151 text += "sincerely,\n"
152 text += " -The Buildbot\n"
153 text += "\n"
154 return { 'body' : text, 'type' : 'plain' }
155
157 """This is a status notifier which sends email to a list of recipients
158 upon the completion of each build. It can be configured to only send out
159 mail for certain builds, and only send messages when the build fails, or
160 when it transitions from success to failure. It can also be configured to
161 include various build logs in each message.
162
163 By default, the message will be sent to the Interested Users list, which
164 includes all developers who made changes in the build. You can add
165 additional recipients with the extraRecipients argument.
166
167 To get a simple one-message-per-build (say, for a mailing list), use
168 sendToInterestedUsers=False, extraRecipients=['listaddr@example.org']
169
170 Each MailNotifier sends mail to a single set of recipients. To send
171 different kinds of mail to different recipients, use multiple
172 MailNotifiers.
173 """
174
175 implements(interfaces.IEmailSender)
176
177 compare_attrs = ["extraRecipients", "lookup", "fromaddr", "mode",
178 "categories", "builders", "addLogs", "relayhost",
179 "subject", "sendToInterestedUsers", "customMesg",
180 "messageFormatter", "extraHeaders"]
181
182 possible_modes = ("change", "failing", "passing", "problem", "warnings", "exception")
183
184 - def __init__(self, fromaddr, mode=("failing", "passing", "warnings"),
185 categories=None, builders=None, addLogs=False,
186 relayhost="localhost", buildSetSummary=False,
187 subject="buildbot %(result)s in %(title)s on %(builder)s",
188 lookup=None, extraRecipients=[],
189 sendToInterestedUsers=True, customMesg=None,
190 messageFormatter=defaultMessage, extraHeaders=None,
191 addPatch=True, useTls=False,
192 smtpUser=None, smtpPassword=None, smtpPort=25):
193 """
194 @type fromaddr: string
195 @param fromaddr: the email address to be used in the 'From' header.
196 @type sendToInterestedUsers: boolean
197 @param sendToInterestedUsers: if True (the default), send mail to all
198 of the Interested Users. If False, only
199 send mail to the extraRecipients list.
200
201 @type extraRecipients: tuple of strings
202 @param extraRecipients: a list of email addresses to which messages
203 should be sent (in addition to the
204 InterestedUsers list, which includes any
205 developers who made Changes that went into this
206 build). It is a good idea to create a small
207 mailing list and deliver to that, then let
208 subscribers come and go as they please. The
209 addresses in this list are used literally (they
210 are not processed by lookup).
211
212 @type subject: string
213 @param subject: a string to be used as the subject line of the message.
214 %(builder)s will be replaced with the name of the
215 builder which provoked the message.
216
217 @type mode: list of strings
218 @param mode: a list of MailNotifer.possible_modes:
219 - "change": send mail about builds which change status
220 - "failing": send mail about builds which fail
221 - "passing": send mail about builds which succeed
222 - "problem": send mail about a build which failed
223 when the previous build passed
224 - "warnings": send mail if a build contain warnings
225 - "exception": send mail if a build fails due to an exception
226 - "all": always send mail
227 Defaults to ("failing", "passing", "warnings").
228
229 @type builders: list of strings
230 @param builders: a list of builder names for which mail should be
231 sent. Defaults to None (send mail for all builds).
232 Use either builders or categories, but not both.
233
234 @type categories: list of strings
235 @param categories: a list of category names to serve status
236 information for. Defaults to None (all
237 categories). Use either builders or categories,
238 but not both.
239
240 @type addLogs: boolean
241 @param addLogs: if True, include all build logs as attachments to the
242 messages. These can be quite large. This can also be
243 set to a list of log names, to send a subset of the
244 logs. Defaults to False.
245
246 @type addPatch: boolean
247 @param addPatch: if True, include the patch when the source stamp
248 includes one.
249
250 @type relayhost: string
251 @param relayhost: the host to which the outbound SMTP connection
252 should be made. Defaults to 'localhost'
253
254 @type buildSetSummary: boolean
255 @param buildSetSummary: if True, this notifier will only send a summary
256 email when a buildset containing any of its
257 watched builds completes
258
259 @type lookup: implementor of {IEmailLookup}
260 @param lookup: object which provides IEmailLookup, which is
261 responsible for mapping User names for Interested
262 Users (which come from the VC system) into valid
263 email addresses. If not provided, the notifier will
264 only be able to send mail to the addresses in the
265 extraRecipients list. Most of the time you can use a
266 simple Domain instance. As a shortcut, you can pass
267 as string: this will be treated as if you had provided
268 Domain(str). For example, lookup='twistedmatrix.com'
269 will allow mail to be sent to all developers whose SVN
270 usernames match their twistedmatrix.com account names.
271
272 @type customMesg: func
273 @param customMesg: (this function is deprecated)
274
275 @type messageFormatter: func
276 @param messageFormatter: function taking (mode, name, build, result,
277 master_status) and returning a dictionary
278 containing two required keys "body" and "type",
279 with a third optional key, "subject". The
280 "body" key gives a string that contains the
281 complete text of the message. The "type" key
282 is the message type ('plain' or 'html'). The
283 'html' type should be used when generating an
284 HTML message. The optional "subject" key
285 gives the subject for the email.
286
287 @type extraHeaders: dict
288 @param extraHeaders: A dict of extra headers to add to the mail. It's
289 best to avoid putting 'To', 'From', 'Date',
290 'Subject', or 'CC' in here. Both the names and
291 values may be WithProperties instances.
292
293 @type useTls: boolean
294 @param useTls: Send emails using TLS and authenticate with the
295 smtp host. Defaults to False.
296
297 @type smtpUser: string
298 @param smtpUser: The user that will attempt to authenticate with the
299 relayhost when useTls is True.
300
301 @type smtpPassword: string
302 @param smtpPassword: The password that smtpUser will use when
303 authenticating with relayhost.
304
305 @type smtpPort: int
306 @param smtpPort: The port that will be used when connecting to the
307 relayhost. Defaults to 25.
308 """
309 base.StatusReceiverMultiService.__init__(self)
310
311 if not isinstance(extraRecipients, (list, tuple)):
312 config.error("extraRecipients must be a list or tuple")
313 else:
314 for r in extraRecipients:
315 if not isinstance(r, str) or not VALID_EMAIL.search(r):
316 config.error(
317 "extra recipient %r is not a valid email" % (r,))
318 self.extraRecipients = extraRecipients
319 self.sendToInterestedUsers = sendToInterestedUsers
320 self.fromaddr = fromaddr
321 if isinstance(mode, basestring):
322 if mode == "all":
323 mode = ("failing", "passing", "warnings", "exception")
324 elif mode == "warnings":
325 mode = ("failing", "warnings")
326 else:
327 mode = (mode,)
328 for m in mode:
329 if m not in self.possible_modes:
330 config.error(
331 "mode %s is not a valid mode" % (m,))
332 self.mode = mode
333 self.categories = categories
334 self.builders = builders
335 self.addLogs = addLogs
336 self.relayhost = relayhost
337 if '\n' in subject:
338 config.error(
339 'Newlines are not allowed in email subjects')
340 self.subject = subject
341 if lookup is not None:
342 if type(lookup) is str:
343 lookup = Domain(lookup)
344 assert interfaces.IEmailLookup.providedBy(lookup)
345 self.lookup = lookup
346 self.customMesg = customMesg
347 self.messageFormatter = messageFormatter
348 if extraHeaders:
349 if not isinstance(extraHeaders, dict):
350 config.error("extraHeaders must be a dictionary")
351 self.extraHeaders = extraHeaders
352 self.addPatch = addPatch
353 self.useTls = useTls
354 self.smtpUser = smtpUser
355 self.smtpPassword = smtpPassword
356 self.smtpPort = smtpPort
357 self.buildSetSummary = buildSetSummary
358 self.buildSetSubscription = None
359 self.watched = []
360 self.master_status = None
361
362 # you should either limit on builders or categories, not both
363 if self.builders != None and self.categories != None:
364 config.error(
365 "Please specify only builders or categories to include - " +
366 "not both.")
367
368 if customMesg:
369 config.error(
370 "customMesg is deprecated; use messageFormatter instead")
371
373 """
374 @type parent: L{buildbot.master.BuildMaster}
375 """
376 base.StatusReceiverMultiService.setServiceParent(self, parent)
377 self.master_status = self.parent
378 self.master_status.subscribe(self)
379 self.master = self.master_status.master
380
382 if self.buildSetSummary:
383 self.buildSetSubscription = \
384 self.master.subscribeToBuildsetCompletions(self.buildsetFinished)
385
386 base.StatusReceiverMultiService.startService(self)
387
389 if self.buildSetSubscription is not None:
390 self.buildSetSubscription.unsubscribe()
391 self.buildSetSubscription = None
392
393 return base.StatusReceiverMultiService.stopService(self)
394
396 self.master_status.unsubscribe(self)
397 self.master_status = None
398 for w in self.watched:
399 w.unsubscribe(self)
400 return base.StatusReceiverMultiService.disownServiceParent(self)
401
403 # only subscribe to builders we are interested in
404 if self.categories != None and builder.category not in self.categories:
405 return None
406
407 self.watched.append(builder)
408 return self # subscribe to this builder
409
412
415
418
420 # here is where we actually do something.
421 builder = build.getBuilder()
422 if self.builders is not None and builder.name not in self.builders:
423 return False # ignore this build
424 if self.categories is not None and \
425 builder.category not in self.categories:
426 return False # ignore this build
427
428 prev = build.getPreviousBuild()
429 if "change" in self.mode:
430 if prev and prev.getResults() != results:
431 return True
432 if "failing" in self.mode and results == FAILURE:
433 return True
434 if "passing" in self.mode and results == SUCCESS:
435 return True
436 if "problem" in self.mode and results == FAILURE:
437 if prev and prev.getResults() != FAILURE:
438 return True
439 if "warnings" in self.mode and results == WARNINGS:
440 return True
441 if "exception" in self.mode and results == EXCEPTION:
442 return True
443
444 return False
445
447 if ( not self.buildSetSummary and
448 self.isMailNeeded(build, results) ):
449 # for testing purposes, buildMessage returns a Deferred that fires
450 # when the mail has been sent. To help unit tests, we return that
451 # Deferred here even though the normal IStatusReceiver.buildFinished
452 # signature doesn't do anything with it. If that changes (if
453 # .buildFinished's return value becomes significant), we need to
454 # rearrange this.
455 return self.buildMessage(name, [build], results)
456 return None
457
459 builds = []
460 for (builddictlist, builder) in res:
461 for builddict in builddictlist:
462 build = builder.getBuild(builddict['number'])
463 if build is not None and self.isMailNeeded(build, build.results):
464 builds.append(build)
465
466 if builds:
467 self.buildMessage("Buildset Complete: " + buildset['reason'], builds,
468 buildset['results'])
469
471 dl = []
472 for breq in breqs:
473 buildername = breq['buildername']
474 builder = self.master_status.getBuilder(buildername)
475 d = self.master.db.builds.getBuildsForRequest(breq['brid'])
476 d.addCallback(lambda builddictlist, builder=builder:
477 (builddictlist, builder))
478 dl.append(d)
479 d = defer.gatherResults(dl)
480 d.addCallback(self._gotBuilds, buildset)
481
483 d = self.master.db.buildrequests.getBuildRequests(bsid=bsid)
484 d.addCallback(self._gotBuildRequests, buildset)
485
487 d = self.master.db.buildsets.getBuildset(bsid=bsid)
488 d.addCallback(self._gotBuildSet, bsid)
489
490 return d
491
493 #
494 # logs is a list of tuples that contain the log
495 # name, log url, and the log contents as a list of strings.
496 #
497 logs = list()
498 for logf in build.getLogs():
499 logStep = logf.getStep()
500 stepName = logStep.getName()
501 logStatus, dummy = logStep.getResults()
502 logName = logf.getName()
503 logs.append(('%s.%s' % (stepName, logName),
504 '%s/steps/%s/logs/%s' % (
505 master_status.getURLForThing(build),
506 stepName, logName),
507 logf.getText().splitlines(),
508 logStatus))
509
510 attrs = {'builderName': name,
511 'title': master_status.getTitle(),
512 'mode': mode,
513 'result': Results[results],
514 'buildURL': master_status.getURLForThing(build),
515 'buildbotURL': master_status.getBuildbotURL(),
516 'buildText': build.getText(),
517 'buildProperties': build.getProperties(),
518 'slavename': build.getSlavename(),
519 'reason': build.getReason().replace('\n', ''),
520 'responsibleUsers': build.getResponsibleUsers(),
521 'branch': "",
522 'revision': "",
523 'patch': "",
524 'patch_info': "",
525 'changes': [],
526 'logs': logs}
527
528 ss = None
529 ss_list = build.getSourceStamps()
530
531 if ss_list:
532 if len(ss_list) == 1:
533 ss = ss_list[0]
534 if ss:
535 attrs['branch'] = ss.branch
536 attrs['revision'] = ss.revision
537 attrs['patch'] = ss.patch
538 attrs['patch_info'] = ss.patch_info
539 attrs['changes'] = ss.changes[:]
540 else:
541 for key in ['branch', 'revision', 'patch', 'patch_info', 'changes']:
542 attrs[key] = {}
543 for ss in ss_list:
544 attrs['branch'][ss.codebase] = ss.branch
545 attrs['revision'][ss.codebase] = ss.revision
546 attrs['patch'][ss.codebase] = ss.patch
547 attrs['patch_info'][ss.codebase] = ss.patch_info
548 attrs['changes'][ss.codebase] = ss.changes[:]
549
550 return attrs
551
553 # patches don't come with an encoding. If the patch is valid utf-8,
554 # we'll attach it as MIMEText; otherwise, it gets attached as a binary
555 # file. This will suit the vast majority of cases, since utf8 is by
556 # far the most common encoding.
557 if type(patch[1]) != types.UnicodeType:
558 try:
559 unicode = patch[1].decode('utf8')
560 except UnicodeDecodeError:
561 unicode = None
562 else:
563 unicode = patch[1]
564
565 if unicode:
566 a = MIMEText(unicode.encode(ENCODING), _charset=ENCODING)
567 else:
568 # MIMEApplication is not present in Python-2.4 :(
569 a = MIMENonMultipart('application', 'octet-stream')
570 a.set_payload(patch[1])
571 a.add_header('Content-Disposition', "attachment",
572 filename="source patch " + str(index) )
573 return a
574
575 - def createEmail(self, msgdict, builderName, title, results, builds=None,
576 patches=None, logs=None):
577 text = msgdict['body'].encode(ENCODING)
578 type = msgdict['type']
579 if 'subject' in msgdict:
580 subject = msgdict['subject'].encode(ENCODING)
581 else:
582 subject = self.subject % { 'result': Results[results],
583 'projectName': title,
584 'title': title,
585 'builder': builderName,
586 }
587
588 assert '\n' not in subject, \
589 "Subject cannot contain newlines"
590
591 assert type in ('plain', 'html'), \
592 "'%s' message type must be 'plain' or 'html'." % type
593
594 if patches or logs:
595 m = MIMEMultipart()
596 m.attach(MIMEText(text, type, ENCODING))
597 else:
598 m = Message()
599 m.set_payload(text, ENCODING)
600 m.set_type("text/%s" % type)
601
602 m['Date'] = formatdate(localtime=True)
603 m['Subject'] = subject
604 m['From'] = self.fromaddr
605 # m['To'] is added later
606
607 if patches:
608 for (i, patch) in enumerate(patches):
609 a = self.patch_to_attachment(patch, i)
610 m.attach(a)
611 if logs:
612 for log in logs:
613 name = "%s.%s" % (log.getStep().getName(),
614 log.getName())
615 if ( self._shouldAttachLog(log.getName()) or
616 self._shouldAttachLog(name) ):
617 text = log.getText()
618 if not isinstance(text, unicode):
619 # guess at the encoding, and use replacement symbols
620 # for anything that's not in that encoding
621 text = text.decode(LOG_ENCODING, 'replace')
622 a = MIMEText(text.encode(ENCODING),
623 _charset=ENCODING)
624 a.add_header('Content-Disposition', "attachment",
625 filename=name)
626 m.attach(a)
627
628 #@todo: is there a better way to do this?
629 # Add any extra headers that were requested, doing WithProperties
630 # interpolation if only one build was given
631 if self.extraHeaders:
632 if len(builds) == 1:
633 d = builds[0].render(self.extraHeaders)
634 else:
635 d = defer.succeed(self.extraHeaders)
636 @d.addCallback
637 def addExtraHeaders(extraHeaders):
638 for k,v in extraHeaders.items():
639 if k in m:
640 twlog.msg("Warning: Got header " + k +
641 " in self.extraHeaders "
642 "but it already exists in the Message - "
643 "not adding it.")
644 m[k] = v
645 d.addCallback(lambda _: m)
646 return d
647
648 return defer.succeed(m)
649
651 if self.customMesg:
652 # the customMesg stuff can be *huge*, so we prefer not to load it
653 attrs = self.getCustomMesgData(self.mode, name, build, results,
654 self.master_status)
655 text, type = self.customMesg(attrs)
656 msgdict = { 'body' : text, 'type' : type }
657 else:
658 msgdict = self.messageFormatter(self.mode, name, build, results,
659 self.master_status)
660
661 return msgdict
662
663
665 patches = []
666 logs = []
667 msgdict = {"body":""}
668
669 for build in builds:
670 ss_list = build.getSourceStamps()
671 if self.addPatch:
672 for ss in ss_list:
673 if ss.patch:
674 patches.append(ss.patch)
675 if self.addLogs:
676 logs.extend(build.getLogs())
677
678 tmp = self.buildMessageDict(name=build.getBuilder().name,
679 build=build, results=build.results)
680 msgdict['body'] += tmp['body']
681 msgdict['body'] += '\n\n'
682 msgdict['type'] = tmp['type']
683 if "subject" in tmp:
684 msgdict['subject'] = tmp['subject']
685
686 d = self.createEmail(msgdict, name, self.master_status.getTitle(),
687 results, builds, patches, logs)
688
689 @d.addCallback
690 def getRecipients(m):
691 # now, who is this message going to?
692 if self.sendToInterestedUsers:
693 dl = []
694 for build in builds:
695 if self.lookup:
696 d = self.useLookup(build)
697 else:
698 d = self.useUsers(build)
699 dl.append(d)
700 d = defer.gatherResults(dl)
701 else:
702 d = defer.succeed([])
703 d.addCallback(self._gotRecipients, m)
704 return d
705
707 dl = []
708 for u in build.getInterestedUsers():
709 d = defer.maybeDeferred(self.lookup.getAddress, u)
710 dl.append(d)
711 return defer.gatherResults(dl)
712
714 dl = []
715 ss = None
716 ss_list = build.getSourceStamps()
717 for ss in ss_list:
718 for change in ss.changes:
719 d = self.master.db.changes.getChangeUids(change.number)
720 def getContacts(uids):
721 def uidContactPair(contact, uid):
722 return (contact, uid)
723 contacts = []
724 for uid in uids:
725 d = users.getUserContact(self.master,
726 contact_type='email',
727 uid=uid)
728 d.addCallback(lambda contact: uidContactPair(contact, uid))
729 contacts.append(d)
730 return defer.gatherResults(contacts)
731 d.addCallback(getContacts)
732 def logNoMatch(contacts):
733 for pair in contacts:
734 contact, uid = pair
735 if contact is None:
736 twlog.msg("Unable to find email for uid: %r" % uid)
737 return [pair[0] for pair in contacts]
738 d.addCallback(logNoMatch)
739 def addOwners(recipients):
740 owners = [e for e in build.getInterestedUsers()
741 if e not in build.getResponsibleUsers()]
742 recipients.extend(owners)
743 return recipients
744 d.addCallback(addOwners)
745 dl.append(d)
746 d = defer.gatherResults(dl)
747 @d.addCallback
748 def gatherRecipients(res):
749 recipients = []
750 map(recipients.extend, res)
751 return recipients
752 return d
753
758
760 to_recipients = set()
761 cc_recipients = set()
762
763 for r in reduce(list.__add__, rlist, []):
764 if r is None: # getAddress didn't like this address
765 continue
766
767 # Git can give emails like 'User' <user@foo.com>@foo.com so check
768 # for two @ and chop the last
769 if r.count('@') > 1:
770 r = r[:r.rindex('@')]
771
772 if VALID_EMAIL.search(r):
773 to_recipients.add(r)
774 else:
775 twlog.msg("INVALID EMAIL: %r" + r)
776
777 # If we're sending to interested users put the extras in the
778 # CC list so they can tell if they are also interested in the
779 # change:
780 if self.sendToInterestedUsers and to_recipients:
781 cc_recipients.update(self.extraRecipients)
782 else:
783 to_recipients.update(self.extraRecipients)
784
785 m['To'] = ", ".join(sorted(to_recipients))
786 if cc_recipients:
787 m['CC'] = ", ".join(sorted(cc_recipients))
788
789 return self.sendMessage(m, list(to_recipients | cc_recipients))
790
792 result = defer.Deferred()
793
794 if have_ssl and self.useTls:
795 client_factory = ssl.ClientContextFactory()
796 client_factory.method = SSLv3_METHOD
797 else:
798 client_factory = None
799
800 if self.smtpUser and self.smtpPassword:
801 useAuth = True
802 else:
803 useAuth = False
804
805 if not ESMTPSenderFactory:
806 raise RuntimeError("twisted-mail is not installed - cannot "
807 "send mail")
808 sender_factory = ESMTPSenderFactory(
809 self.smtpUser, self.smtpPassword,
810 self.fromaddr, recipients, StringIO(s),
811 result, contextFactory=client_factory,
812 requireTransportSecurity=self.useTls,
813 requireAuthentication=useAuth)
814
815 reactor.connectTCP(self.relayhost, self.smtpPort, sender_factory)
816
817 return result
818
820 s = m.as_string()
821 twlog.msg("sending mail (%d bytes) to" % len(s), recipients)
822 return self.sendmail(s, recipients)
823
| Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Wed Nov 21 16:23:05 2012 | http://epydoc.sourceforge.net |