root/announcerplugin/trunk/announcer/distributors/mail.py

Revision 9227, 25.9 kB (checked in by doki_pen, 2 years ago)

Removes some more deprecated code

Line 
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (c) 2008, Stephen Hansen
4 # Copyright (c) 2009, Robert Corsaro
5 # Copyright (c) 2010, Steffen Hoffmann
6 #
7 # All rights reserved.
8 #
9 # Redistribution and use in source and binary forms, with or without
10 # modification, are permitted provided that the following conditions are met:
11 #
12 #     * Redistributions of source code must retain the above copyright
13 #       notice, this list of conditions and the following disclaimer.
14 #     * Redistributions in binary form must reproduce the above copyright
15 #       notice, this list of conditions and the following disclaimer in the
16 #       documentation and/or other materials provided with the distribution.
17 #     * Neither the name of the <ORGANIZATION> nor the names of its
18 #       contributors may be used to endorse or promote products derived from
19 #       this software without specific prior written permission.
20 #
21 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
25 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
26 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
27 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
28 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
29 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
30 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 # ----------------------------------------------------------------------------
33
34 # TODO: pick format based on subscription.  For now users will use the same
35 #       format for all announcements, but in the future we can make this more
36 #       flexible, since it's in the subscription table.
37 import Queue
38 import random
39 import re
40 import smtplib
41 import sys
42 import threading
43 import time
44
45 from subprocess import Popen, PIPE
46
47 from email.MIMEMultipart import MIMEMultipart
48 from email.MIMEText import MIMEText
49 from email.Utils import formatdate, formataddr
50 from email.Charset import Charset, QP, BASE64
51 try:
52     from email.header import Header
53 except:
54     from email.Header import Header
55
56 from trac.core import *
57 from trac.util.compat import set, sorted
58 from trac.config import Option, BoolOption, IntOption, \
59     OrderedExtensionsOption, ChoiceOption
60 from trac.config import ExtensionOption
61 from trac.util import get_pkginfo, md5
62 from trac.util.datefmt import to_timestamp
63 from trac.util.text import to_unicode, CRLF
64
65 from announcer.api import AnnouncementSystem
66 from announcer.api import IAnnouncementAddressResolver
67 from announcer.api import IAnnouncementDistributor
68 from announcer.api import IAnnouncementFormatter
69 from announcer.api import IAnnouncementPreferenceProvider
70 from announcer.api import IAnnouncementProducer
71 from announcer.api import _
72
73 from announcer.util.mail import set_header
74 from announcer.util.mail_crypto import CryptoTxt
75
76
77 class IEmailSender(Interface):
78     """Extension point interface for components that allow sending e-mail."""
79
80     def send(self, from_addr, recipients, message):
81         """Send message to recipients."""
82
83
84 class IAnnouncementEmailDecorator(Interface):
85     def decorate_message(event, message, decorators):
86         """Manipulate the message before it is sent on it's way.  The callee
87         should call the next decorator by popping decorators and calling the
88         popped decorator.  If decorators is empty, don't worry about it.
89         """
90
91
92 class EmailDistributor(Component):
93
94     implements(IAnnouncementDistributor)
95
96     formatters = ExtensionPoint(IAnnouncementFormatter)
97     # Make ordered
98     decorators = ExtensionPoint(IAnnouncementEmailDecorator)
99
100     resolvers = OrderedExtensionsOption('announcer', 'email_address_resolvers',
101         IAnnouncementAddressResolver, 'SpecifiedEmailResolver, '\
102         'SessionEmailResolver, DefaultDomainEmailResolver',
103         """Comma seperated list of email resolver components in the order
104         they will be called.  If an email address is resolved, the remaining
105         resolvers will not be called.
106         """)
107
108     email_sender = ExtensionOption('announcer', 'email_sender',
109         IEmailSender, 'SmtpEmailSender',
110         """Name of the component implementing `IEmailSender`.
111
112         This component is used by the announcer system to send emails.
113         Currently, `SmtpEmailSender` and `SendmailEmailSender` are provided.
114         """)
115
116     enabled = BoolOption('announcer', 'email_enabled', 'true',
117         """Enable email notification.""")
118
119     email_from = Option('announcer', 'email_from', 'trac@localhost',
120         """Sender address to use in notification emails.""")
121
122     from_name = Option('announcer', 'email_from_name', '',
123         """Sender name to use in notification emails.""")
124
125     replyto = Option('announcer', 'email_replyto', 'trac@localhost',
126         """Reply-To address to use in notification emails.""")
127
128     mime_encoding = ChoiceOption('announcer', 'mime_encoding',
129         ['base64', 'qp', 'none'],
130         """Specifies the MIME encoding scheme for emails.
131
132         Valid options are 'base64' for Base64 encoding, 'qp' for
133         Quoted-Printable, and 'none' for no encoding. Note that the no encoding
134         means that non-ASCII characters in text are going to cause problems
135         with notifications.
136         """)
137
138     use_public_cc = BoolOption('announcer', 'use_public_cc', 'false',
139         """Recipients can see email addresses of other CC'ed recipients.
140
141         If this option is disabled (the default), recipients are put on BCC
142         """)
143
144     # used in email decorators, but not here
145     subject_prefix = Option('announcer', 'email_subject_prefix',
146                                  '__default__',
147         """Text to prepend to subject line of notification emails.
148
149         If the setting is not defined, then the [$project_name] prefix.
150         If no prefix is desired, then specifying an empty option
151         will disable it.
152         """)
153
154     to = Option('announcer', 'email_to', 'undisclosed-recipients: ;',
155         'Default To: field')
156
157     use_threaded_delivery = BoolOption('announcer', 'use_threaded_delivery',
158         'false',
159         """Do message delivery in a separate thread.
160
161         Enabling this will improve responsiveness for requests that end up
162         with an announcement being sent over email. It requires building
163         Python with threading support enabled-- which is usually the case.
164         To test, start Python and type 'import threading' to see
165         if it raises an error.
166         """)
167
168     default_email_format = Option('announcer', 'default_email_format',
169         'text/plain',
170         """The default mime type of the email notifications.
171
172         This can be overridden on a per user basis through the announcer
173         preferences panel.
174         """)
175
176     rcpt_allow_regexp = Option('announcer', 'rcpt_allow_regexp', '',
177         """A whitelist pattern to match any address to before adding to
178         recipients list.
179         """)
180
181     rcpt_local_regexp = Option('announcer', 'rcpt_local_regexp', '',
182         """A whitelist pattern to match any address, that should be
183         considered local.
184
185         This will be evaluated only if msg encryption is set too.
186         Recipients with matching email addresses will continue to
187         receive unencrypted email messages.
188         """)
189
190     crypto = Option('announcer', 'email_crypto', '',
191         """Enable cryptographically operation on email msg body.
192
193         Empty string, the default for unset, disables all crypto operations.
194         Valid values are:
195             sign          sign msg body with given privkey
196             encrypt       encrypt msg body with pubkeys of all recipients
197             sign,encrypt  sign, than encrypt msg body
198         """)
199
200     # get GnuPG configuration options
201     gpg_binary = Option('announcer', 'gpg_binary', 'gpg',
202         """GnuPG binary name, allows for full path too.
203
204         Value 'gpg' is same default as in python-gnupg itself.
205         For usual installations location of the gpg binary is auto-detected.
206         """)
207
208     gpg_home = Option('announcer', 'gpg_home', '',
209         """Directory containing keyring files.
210
211         In case of wrong configuration missing keyring files without content
212         will be created in the configured location, provided necessary
213         write permssion is granted for the corresponding parent directory.
214         """)
215
216     private_key = Option('announcer', 'gpg_signing_key', None,
217         """Keyid of private key (last 8 chars or more) used for signing.
218
219         If unset, a private key will be selected from keyring automagicly.
220         The password must be available i.e. provided by running gpg-agent
221         or empty (bad security). On failing to unlock the private key,
222         msg body will get emptied.
223         """)
224
225
226     def __init__(self):
227         self.delivery_queue = None
228         self._init_pref_encoding()
229
230     def get_delivery_queue(self):
231         if not self.delivery_queue:
232             self.delivery_queue = Queue.Queue()
233             thread = DeliveryThread(self.delivery_queue, self.send)
234             thread.start()
235         return self.delivery_queue
236
237     # IAnnouncementDistributor
238     def transports(self):
239         yield "email"
240
241     def formats(self, transport, realm):
242         "Find valid formats for transport and realm"
243         formats = {}
244         for f in self.formatters:
245             for style in f.styles(transport, realm):
246                 formats[style] = f
247         self.log.debug(
248             "EmailDistributor has found the following formats capable "
249             "of handling '%s' of '%s': %s"%(transport, realm,
250                 ', '.join(formats.keys())))
251         if not formats:
252             self.log.error("EmailDistributor is unable to continue " \
253                     "without supporting formatters.")
254         return formats
255
256     def distribute(self, transport, recipients, event):
257         found = False
258         for supported_transport in self.transports():
259             if supported_transport == transport:
260                 found = True
261         if not self.enabled or not found:
262             self.log.debug("EmailDistributer email_enabled set to false")
263             return
264         fmtdict = self.formats(transport, event.realm)
265         if not fmtdict:
266             self.log.error(
267                 "EmailDistributer No formats found for %s %s"%(
268                     transport, event.realm))
269             return
270         msgdict = {}
271         msgdict_encrypt = {}
272         msg_pubkey_ids = []
273         # compile pattern before use for better performance
274         RCPT_ALLOW_RE = re.compile(self.rcpt_allow_regexp)
275         RCPT_LOCAL_RE = re.compile(self.rcpt_local_regexp)
276
277         if self.crypto != '':
278             self.log.debug("EmailDistributor attempts crypto operation.")
279             self.enigma = CryptoTxt(self.gpg_binary, self.gpg_home)
280
281         for name, authed, addr in recipients:
282             fmt = name and \
283                 self._get_preferred_format(event.realm, name, authed) or \
284                 self._get_default_format()
285             if fmt not in fmtdict:
286                 self.log.debug(("EmailDistributer format %s not available " +
287                     "for %s %s, looking for an alternative")%(
288                         fmt, transport, event.realm))
289                 # If the fmt is not available for this realm, then try to find
290                 # an alternative
291                 oldfmt = fmt
292                 fmt = None
293                 for f in fmtdict.values():
294                     fmt = f.alternative_style_for(
295                             transport, event.realm, oldfmt)
296                     if fmt: break
297             if not fmt:
298                 self.log.error(
299                     "EmailDistributer was unable to find a formatter " +
300                     "for format %s"%k
301                 )
302                 continue
303             rslvr = None
304             if name and not addr:
305                 # figure out what the addr should be if it's not defined
306                 for rslvr in self.resolvers:
307                     addr = rslvr.get_address_for_name(name, authed)
308                     if addr: break
309             if addr:
310                 self.log.debug("EmailDistributor found the " \
311                         "address '%s' for '%s (%s)' via: %s"%(
312                         addr, name, authed and \
313                         'authenticated' or 'not authenticated',
314                         rslvr.__class__.__name__))
315
316                 # ok, we found an addr, add the message
317                 # but wait, check for allowed rcpt first, if set
318                 if RCPT_ALLOW_RE.search(addr) is not None:
319                     # check for local recipients now
320                     local_match = RCPT_LOCAL_RE.search(addr)
321                     if self.crypto in ['encrypt', 'sign,encrypt'] and \
322                             local_match is None:
323                         # search available public keys for matching UID
324                         pubkey_ids = self.enigma.get_pubkey_ids(addr)
325                         if len(pubkey_ids) > 0:
326                             msgdict_encrypt.setdefault(fmt, set()).add((name,
327                                                             authed, addr))
328                             msg_pubkey_ids[len(msg_pubkey_ids):] = pubkey_ids
329                             self.log.debug("EmailDistributor got pubkeys " \
330                                 "for %s: %s" % (addr, pubkey_ids))
331                         else:
332                             self.log.debug("EmailDistributor dropped %s " \
333                                 "after missing pubkey with corresponding " \
334                                 "address %s in any UID" % (name, addr))
335                     else:
336                         msgdict.setdefault(fmt, set()).add((name, authed,
337                                                             addr))
338                         if local_match is not None:
339                             self.log.debug("EmailDistributor expected " \
340                                 "local delivery for %s to: %s" % (name, addr))
341                 else:
342                     self.log.debug("EmailDistributor dropped %s for " \
343                         "not matching allowed recipient pattern %s" % \
344                         (addr, self.rcpt_allow_regexp))
345             else:
346                 self.log.debug("EmailDistributor was unable to find an " \
347                         "address for: %s (%s)"%(name, authed and \
348                         'authenticated' or 'not authenticated'))
349         for k, v in msgdict.items():
350             if not v or not fmtdict.get(k):
351                 continue
352             self.log.debug(
353                 "EmailDistributor is sending event as '%s' to: %s"%(
354                     fmt, ', '.join(x[2] for x in v)))
355             self._do_send(transport, event, k, v, fmtdict[k])
356         for k, v in msgdict_encrypt.items():
357             if not v or not fmtdict.get(k):
358                 continue
359             self.log.debug(
360                 "EmailDistributor is sending encrypted info on event " \
361                 "as '%s' to: %s"%(fmt, ', '.join(x[2] for x in v)))
362             self._do_send(transport, event, k, v, fmtdict[k], msg_pubkey_ids)
363
364     def _get_default_format(self):
365         return self.default_email_format
366
367     def _get_preferred_format(self, realm, sid, authenticated):
368         if authenticated is None:
369             authenticated = 0
370         db = self.env.get_db_cnx()
371         cursor = db.cursor()
372         cursor.execute("""
373             SELECT value
374               FROM session_attribute
375              WHERE sid=%s
376                AND authenticated=%s
377                AND name=%s
378         """, (sid, int(authenticated), 'announcer_email_format_%s' % realm))
379         result = cursor.fetchone()
380         if result:
381             chosen = result[0]
382             self.log.debug("EmailDistributor determined the preferred format" \
383                     " for '%s (%s)' is: %s"%(sid, authenticated and \
384                     'authenticated' or 'not authenticated', chosen))
385             return chosen
386         else:
387             return self._get_default_format()
388
389     def _init_pref_encoding(self):
390         self._charset = Charset()
391         self._charset.input_charset = 'utf-8'
392         pref = self.mime_encoding.lower()
393         if pref == 'base64':
394             self._charset.header_encoding = BASE64
395             self._charset.body_encoding = BASE64
396             self._charset.output_charset = 'utf-8'
397             self._charset.input_codec = 'utf-8'
398             self._charset.output_codec = 'utf-8'
399         elif pref in ['qp', 'quoted-printable']:
400             self._charset.header_encoding = QP
401             self._charset.body_encoding = QP
402             self._charset.output_charset = 'utf-8'
403             self._charset.input_codec = 'utf-8'
404             self._charset.output_codec = 'utf-8'
405         elif pref == 'none':
406             self._charset.header_encoding = None
407             self._charset.body_encoding = None
408             self._charset.input_codec = None
409             self._charset.output_charset = 'ascii'
410         else:
411             raise TracError(_('Invalid email encoding setting: %s'%pref))
412
413     def _message_id(self, realm):
414         """Generate a predictable, but sufficiently unique message ID."""
415         modtime = time.time()
416         rand = random.randint(0,32000)
417         s = '%s.%d.%d.%s' % (self.env.project_url,
418                           modtime, rand,
419                           realm.encode('ascii', 'ignore'))
420         dig = md5(s).hexdigest()
421         host = self.email_from[self.email_from.find('@') + 1:]
422         msgid = '<%03d.%s@%s>' % (len(s), dig, host)
423         return msgid
424
425     def _filter_recipients(self, rcpt):
426         return rcpt
427
428     def _do_send(self, transport, event, format, recipients, formatter,
429                  pubkey_ids=[]):
430
431         output = formatter.format(transport, event.realm, format, event)
432
433         # DEVEL: force message body plaintext style for crypto operations
434         if self.crypto != '' and pubkey_ids != []:
435             if self.crypto == 'sign':
436                 output = self.enigma.sign(output, self.private_key)
437             elif self.crypto == 'encrypt':
438                 output = self.enigma.encrypt(output, pubkey_ids)
439             elif self.crypto == 'sign,encrypt':
440                 output = self.enigma.sign_encrypt(output, pubkey_ids,
441                                                      self.private_key)
442
443             self.log.debug(output)
444             self.log.debug(_("EmailDistributor crypto operaton successful."))
445             alternate_output = None
446         else:
447             alternate_style = formatter.alternative_style_for(
448                 transport,
449                 event.realm,
450                 format
451             )
452             if alternate_style:
453                 alternate_output = formatter.format(
454                     transport,
455                     event.realm,
456                     alternate_style,
457                     event
458                 )
459             else:
460                 alternate_output = None
461
462         # sanity check
463         if not self._charset.body_encoding:
464             try:
465                 dummy = output.encode('ascii')
466             except UnicodeDecodeError:
467                 raise TracError(_("Ticket contains non-ASCII chars. " \
468                                   "Please change encoding setting"))
469
470         rootMessage = MIMEMultipart("related")
471         # TODO: is this good? (from jabber branch)
472         #rootMessage.set_charset(self._charset)
473
474         headers = dict()
475         headers['Message-ID'] = self._message_id(event.realm)
476         headers['Date'] = formatdate()
477         from_header = formataddr((
478             self.from_name or self.env.project_name,
479             self.email_from
480         ))
481         headers['From'] = from_header
482         headers['To'] = '"%s"'%(self.to)
483         if self.use_public_cc:
484             headers['Cc'] = ', '.join([x[2] for x in recipients if x])
485         headers['Reply-To'] = self.replyto
486         for k, v in headers.iteritems():
487             set_header(rootMessage, k, v)
488
489         rootMessage.preamble = 'This is a multi-part message in MIME format.'
490         if alternate_output:
491             parentMessage = MIMEMultipart('alternative')
492             rootMessage.attach(parentMessage)
493
494             alt_msg_format = 'html' in alternate_style and 'html' or 'plain'
495             msgText = MIMEText(alternate_output, alt_msg_format)
496             parentMessage.attach(msgText)
497         else:
498             parentMessage = rootMessage
499
500         msg_format = 'html' in format and 'html' or 'plain'
501         msgText = MIMEText(output, msg_format)
502         del msgText['Content-Transfer-Encoding']
503         msgText.set_charset(self._charset)
504         parentMessage.attach(msgText)
505         decorators = self._get_decorators()
506         if len(decorators) > 0:
507             decorator = decorators.pop()
508             decorator.decorate_message(event, rootMessage, decorators)
509
510         recip_adds = [x[2] for x in recipients if x]
511         # Append any to, cc or bccs added to the recipient list
512         for field in ('To', 'Cc', 'Bcc'):
513             if rootMessage[field] and \
514                     len(str(rootMessage[field]).split(',')) > 0:
515                 for addy in str(rootMessage[field]).split(','):
516                     self._add_recipient(recip_adds, addy)
517         # replace with localized bcc hint
518         if headers['To'] == 'undisclosed-recipients: ;':
519             set_header(rootMessage, 'To', _('undisclosed-recipients: ;'))
520
521         self.log.debug("Content of recip_adds: %s" %(recip_adds))
522         package = (from_header, recip_adds, rootMessage.as_string())
523         start = time.time()
524         if self.use_threaded_delivery:
525             self.get_delivery_queue().put(package)
526         else:
527             self.send(*package)
528         stop = time.time()
529         self.log.debug("EmailDistributor took %s seconds to send."\
530                 %(round(stop-start,2)))
531
532     def send(self, from_addr, recipients, message):
533         """Send message to recipients via e-mail."""
534         # Ensure the message complies with RFC2822: use CRLF line endings
535         message = CRLF.join(re.split("\r?\n", message))
536         self.email_sender.send(from_addr, recipients, message)
537
538     def _get_decorators(self):
539         return self.decorators[:]
540
541     def _add_recipient(self, recipients, addy):
542         if addy.strip() != '"undisclosed-recipients: ;"':
543             recipients.append(addy)
544
545
546
547 class SmtpEmailSender(Component):
548     """E-mail sender connecting to an SMTP server."""
549
550     implements(IEmailSender)
551
552     server = Option('smtp', 'server', 'localhost',
553         """SMTP server hostname to use for email notifications.""")
554
555     timeout = IntOption('smtp', 'timeout', 10,
556         """SMTP server connection timeout. (requires python-2.6)""")
557
558     port = IntOption('smtp', 'port', 25,
559         """SMTP server port to use for email notification.""")
560
561     user = Option('smtp', 'user', '',
562         """Username for SMTP server.""")
563
564     password = Option('smtp', 'password', '',
565         """Password for SMTP server.""")
566
567     use_tls = BoolOption('smtp', 'use_tls', 'false',
568         """Use SSL/TLS to send notifications over SMTP.""")
569
570     use_ssl = BoolOption('smtp', 'use_ssl', 'false',
571         """Use ssl for smtp connection.""")
572
573     debuglevel = IntOption('smtp', 'debuglevel', 0,
574         """Set to 1 for useful smtp debugging on stdout.""")
575
576
577     def send(self, from_addr, recipients, message):
578         # use defaults to make sure connect() is called in the constructor
579         smtpclass = smtplib.SMTP
580         if self.use_ssl:
581             smtpclass = smtplib.SMTP_SSL
582
583         args = {
584             'host': self.server,
585             'port': self.port
586         }
587         # timeout isn't supported until python 2.6
588         vparts = sys.version_info[0:2]
589         if vparts[0] >= 2 and vparts[1] >= 6:
590             args['timeout'] = self.timeout
591
592         smtp = smtpclass(**args)
593         smtp.set_debuglevel(self.debuglevel)
594         if self.use_tls:
595             smtp.ehlo()
596             if not smtp.esmtp_features.has_key('starttls'):
597                 raise TracError(_("TLS enabled but server does not support " \
598                         "TLS"))
599             smtp.starttls()
600             smtp.ehlo()
601         if self.user:
602             smtp.login(
603                 self.user.encode('utf-8'),
604                 self.password.encode('utf-8')
605             )
606         smtp.sendmail(from_addr, recipients, message)
607         if self.use_tls or self.use_ssl:
608             # avoid false failure detection when the server closes
609             # the SMTP connection with TLS/SSL enabled
610             import socket
611             try:
612                 smtp.quit()
613             except socket.sslerror:
614                 pass
615         else:
616             smtp.quit()
617
618
619 class SendmailEmailSender(Component):
620     """E-mail sender using a locally-installed sendmail program."""
621
622     implements(IEmailSender)
623
624     sendmail_path = Option('sendmail', 'sendmail_path', 'sendmail',
625         """Path to the sendmail executable.
626
627         The sendmail program must accept the `-i` and `-f` options.
628         """)
629
630     def send(self, from_addr, recipients, message):
631         self.log.info("Sending notification through sendmail at %s to %s"
632                       % (self.sendmail_path, recipients))
633         cmdline = [self.sendmail_path, "-i", "-f", from_addr]
634         cmdline.extend(recipients)
635         self.log.debug("Sendmail command line: %s" % ' '.join(cmdline))
636         try:
637             child = Popen(cmdline, bufsize=-1, stdin=PIPE, stdout=PIPE,
638                           stderr=PIPE)
639             (out, err) = child.communicate(message)
640             if child.returncode or err:
641                 raise Exception("Sendmail failed with (%s, %s), command: '%s'"
642                                 % (child.returncode, err.strip(), cmdline))
643         except OSError, e:
644             self.log.error("Failed to run sendmail[%s] with error %s"%\
645                     (self.sendmail_path, e))
646
647
648 class DeliveryThread(threading.Thread):
649     def __init__(self, queue, sender):
650         threading.Thread.__init__(self)
651         self._sender = sender
652         self._queue = queue
653         self.setDaemon(True)
654
655     def run(self):
656         while 1:
657             sendfrom, recipients, message = self._queue.get()
658             self._sender(sendfrom, recipients, message)
Note: See TracBrowser for help on using the browser.