source: emailtotracscript/trunk/email2trac.py.in @ 87

Last change on this file since 87 was 87, checked in by bas, 18 years ago

EmailtoTracScript?:

email2trac.py.in:

  • added enable_syslog option
  • fixed an error in unicode converting
  • Fixed some layout code
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 20.3 KB
Line 
1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
22"""
23email2trac.py -- Email tickets to Trac.
24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
30Changed By: Bas van der Vlies <basv@sara.nl>
31Date      : 13 September 2005
32Descr.    : Added config file and command line options, spam level
33            detection, reply address and mailto option. Unicode support
34
35Changed By: Walter de Jong <walter@sara.nl>
36Descr.    : multipart-message code and trac attachments
37
38
39The scripts reads emails from stdin and inserts directly into a Trac database.
40MIME headers are mapped as follows:
41
42        * From:      => Reporter
43                     => CC (Optional via reply_address option)
44        * Subject:   => Summary
45        * Body       => Description
46        * Component  => Can be set to SPAM via spam_level option
47
48How to use
49----------
50 * Create an config file:
51        [DEFAULT]                      # REQUIRED
52        project      : /data/trac/test # REQUIRED
53        debug        : 1               # OPTIONAL, if set print some DEBUG info
54        spam_level   : 4               # OPTIONAL, if set check for SPAM mail
55        reply_address: 1               # OPTIONAL, if set then fill in ticket CC field
56        umask        : 022             # OPTIONAL, if set then use this umask for creation of the attachments
57        mailto_link  : 1               # OPTIONAL, if set then [mailto:<>] in description
58        mailto_cc    : basv@sara.nl    # OPTIONAL, use this address as CC in mailto line
59        ticket_update: 1               # OPTIONAL, if set then check if this is an update for a ticket
60        trac_version : 0.8             # OPTIONAL, default is 0.9
61
62        [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
63        project      : /data/trac/jouvin # use -p|--project jouvin. 
64       
65 * default config file is : /etc/email2trac.conf
66
67 * Commandline opions:
68                -h | --help
69                -c <value> | --component=<value>
70                -f <config file> | --file=<config file>
71                -p <project name> | --project=<project name>
72
73SVN Info:
74        $Id: email2trac.py.in 87 2006-06-22 12:13:42Z bas $
75"""
76import os
77import sys
78import string
79import getopt
80import stat
81import time
82import email
83import re
84import urllib
85import unicodedata
86import ConfigParser
87from email import Header
88from stat import *
89import mimetypes
90
91trac_default_version = 0.9
92
93class TicketEmailParser(object):
94        env = None
95        comment = '> '
96   
97        def __init__(self, env, parameters, version):
98                self.env = env
99
100                # Database connection
101                #
102                self.db = None
103
104                # Some useful mail constants
105                #
106                self.author = None
107                self.email_addr = None
108                self.email_field = None
109
110                self.VERSION = version
111                if self.VERSION == 0.8:
112                        self.get_config = self.env.get_config
113                else:
114                        self.get_config = self.env.config.get
115
116                if parameters.has_key('umask'):
117                        os.umask(int(parameters['umask'], 8))
118
119                if parameters.has_key('debug'):
120                        self.DEBUG = int(parameters['debug'])
121                else:
122                        self.DEBUG = 0
123
124                if parameters.has_key('mailto_link'):
125                        self.MAILTO = int(parameters['mailto_link'])
126                        if parameters.has_key('mailto_cc'):
127                                self.MAILTO_CC = parameters['mailto_cc']
128                        else:
129                                self.MAILTO_CC = ''
130                else:
131                        self.MAILTO = 0
132
133                if parameters.has_key('spam_level'):
134                        self.SPAM_LEVEL = int(parameters['spam_level'])
135                else:
136                        self.SPAM_LEVEL = 0
137
138                if parameters.has_key('email_comment'):
139                        self.comment = str(parameters['email_comment'])
140
141                if parameters.has_key('email_header'):
142                        self.EMAIL_HEADER = int(parameters['email_header'])
143                else:
144                        self.EMAIL_HEADER = 0
145
146                if parameters.has_key('alternate_notify_template'):
147                        self.notify_template = str(parameters['alternate_notify_template'])
148                else:
149                        self.notify_template = None
150
151                if parameters.has_key('reply_all'):
152                        self.REPLY_ALL = int(parameters['reply_all'])
153                else:
154                        self.REPLY_ALL = 0
155
156                if parameters.has_key('ticket_update'):
157                        self.TICKET_UPDATE = int(parameters['ticket_update'])
158                else:
159                        self.TICKET_UPDATE = 0
160
161
162        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
163        # Note if Spam_level then '*' are included
164        def spam(self, message):
165                if message.has_key('X-Spam-Score'):
166                        spam_l = string.split(message['X-Spam-Score'])
167                        number = spam_l[0].count('*')
168
169                        if number >= self.SPAM_LEVEL:
170                                return 'Spam'
171
172                elif message.has_key('X-Virus-found'):                  # treat virus mails as spam
173                        return 'Spam'
174
175                return self.get_config('ticket', 'default_component')
176
177        def to_unicode(self, str):
178                """
179                Email has 7 bit ASCII code, convert it to unicode with the charset
180        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
181                understands it.
182                """
183                results =  Header.decode_header(str)
184                str = None
185                for text,format in results:
186                        if format:
187                                try:
188                                        temp = unicode(text, format)
189                                except (UnicodeError,LookupError):
190                                        # This always works
191                                        #
192                                        temp = unicode(text, 'iso-8859-15')
193                                       
194                                temp =  temp.encode('utf-8')
195                        else:
196                                temp = string.strip(text)
197
198                        if str:
199                                str = '%s %s' %(str, temp)
200                        else:
201                                str = temp
202
203                return str
204
205        def debug_attachments(self, message):
206                n = 0
207                for part in message.walk():
208                        if part.get_content_maintype() == 'multipart':      # multipart/* is just a container
209                                print 'TD: multipart container'
210                                continue
211
212                        n = n + 1
213                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
214                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
215
216                        if part.is_multipart():
217                                print 'TD: this part is multipart'
218                                payload = part.get_payload(decode=1)
219                                print 'TD: payload:', payload
220                        else:
221                                print 'TD: this part is not multipart'
222
223                        part_file = '/var/tmp/part%d' % n
224                        print 'TD: writing part%d (%s)' % (n,part_file)
225                        fx = open(part_file, 'wb')
226                        text = part.get_payload(decode=1)
227                        if not text:
228                                text = '(None)'
229                        fx.write(text)
230                        fx.close()
231                        try:
232                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
233                        except OSError:
234                                pass
235
236        def email_header_txt(self, m):
237                """
238                Display To and CC addresses in description field
239                """
240                str = ''
241                if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
242                        str = "'''To:''' %s [[BR]]" %(m['To'])
243                if m['Cc'] and len(m['Cc']) > 0:
244                        str = "%s'''Cc:''' %s [[BR]]" % (str, m['Cc'])
245
246                return str
247
248        def set_owner(self, ticket):
249                """
250                Select default owner for ticket component
251                """
252                cursor = self.db.cursor()
253                sql = "SELECT owner FROM component WHERE name='%s'" % ticket['component']
254                cursor.execute(sql)
255                try:
256                        ticket['owner'] = cursor.fetchone()[0]
257                except TypeError, detail:
258                        ticket['owner'] = "UNKNOWN"
259
260        def get_author_emailaddrs(self, message):
261                """
262                Get the default author name and email address from the message
263                """
264                self.author, self.email_addr  = email.Utils.parseaddr(message['from'])
265
266                # Look for email address in registered trac users
267                #
268                if self.VERSION == 0.8:
269                        users = []
270                else:
271                        users = [ u for (u, n, e) in self.env.get_known_users(self.db)
272                                if e == self.email_addr ]
273
274                if len(users) == 1:
275                        self.email_field = users[0]
276                else:
277                        self.email_field =  self.to_unicode(message['from'])
278
279        def set_reply_fields(self, ticket, message):
280                """
281                Set all the right fields for a new ticket
282                """
283                ticket['reporter'] = self.email_field
284
285                # Put all CC-addresses in ticket CC field
286                #
287                if self.REPLY_ALL:
288                        #tos = message.get_all('to', [])
289                        ccs = message.get_all('cc', [])
290
291                        addrs = email.Utils.getaddresses(ccs)
292
293                        # Remove reporter email address if notification is
294                        # on
295                        #
296                        if self.notification:
297                                try:
298                                        addrs.remove((self.author, self.email_addr))
299                                except ValueError, detail:
300                                        pass
301
302                        for name,mail in addrs:
303                                        try:
304                                                ticket['cc'] = '%s,%s' %(ticket['cc'], mail)
305                                        except:
306                                                ticket['cc'] = mail
307
308        def save_email_for_debug(self, message):
309                msg_file = '/var/tmp/msg.txt'
310                print 'TD: saving email to %s' % msg_file
311                fx = open(msg_file, 'wb')
312                fx.write('%s' % message)
313                fx.close()
314                try:
315                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
316                except OSError:
317                        pass
318
319        def ticket_update(self, m):
320                """
321                If the current email is a reply to an existing ticket, this function
322                will append the contents of this email to that ticket, instead of
323                creating a new one.
324                """
325                if not m['Subject']:
326                        return False
327                else:
328                        subject  = self.to_unicode(m['Subject'])
329
330                TICKET_RE = re.compile(r"""
331                                        (?P<ticketnr>[#][0-9]+:)
332                                        """, re.VERBOSE)
333
334                result =  TICKET_RE.search(subject)
335                if not result:
336                        return False
337
338                body_text = self.get_body_text(m)
339                body_text = '{{{\n%s\n}}}' %body_text
340
341                # Strip '#' and ':' from ticket_id
342                #
343                ticket_id = result.group('ticketnr')
344                ticket_id = int(ticket_id[1:-1])
345
346                # Get current time
347                #
348                when = int(time.time())
349
350                if self.VERSION  == 0.8:
351                        tkt = Ticket(self.db, ticket_id)
352                        tkt.save_changes(self.db, self.author, body_text, when)
353                else:
354                        tkt = Ticket(self.env, ticket_id, self.db)
355                        tkt.save_changes(self.author, body_text, when)
356                        tkt['id'] = ticket_id
357
358                self.attachments(m, tkt)
359
360                if self.notification:
361                        self.notify(tkt, False, when)
362
363                return True
364
365        def new_ticket(self, msg):
366                """
367                Create a new ticket
368                """
369                tkt = Ticket(self.env)
370                tkt['status'] = 'new'
371
372                # Some defaults
373                #
374                tkt['milestone'] = self.get_config('ticket', 'default_milestone')
375                tkt['priority'] = self.get_config('ticket', 'default_priority')
376                tkt['severity'] = self.get_config('ticket', 'default_severity')
377                tkt['version'] = self.get_config('ticket', 'default_version')
378
379                if not msg['Subject']:
380                        tkt['summary'] = '(geen subject)'
381                else:
382                        tkt['summary'] = self.to_unicode(msg['Subject'])
383
384
385                if settings.has_key('component'):
386                        tkt['component'] = settings['component']
387                else:
388                        tkt['component'] = self.spam(msg)
389
390                # Must make this an option or so, discard SPAM messages or save then
391                # and delete later
392                #
393                #if self.SPAM_LEVEL and self.spam(msg):
394                #       print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
395                #       sys.exit(1)
396
397                # Set default owner for component
398                #
399                self.set_owner(tkt)
400                self.set_reply_fields(tkt, msg)
401
402                # produce e-mail like header
403                #
404                head = ''
405                if self.EMAIL_HEADER > 0:
406                        head = self.email_header_txt(msg)
407
408
409                body_text = self.get_body_text(msg)
410                tkt['description'] = ''
411
412                # Insert ticket in database with empty description
413                #
414                when = int(time.time())
415                if self.VERSION == 0.8:
416                        tkt['id'] = tkt.insert(self.db)
417                else:
418                        tkt['id'] = tkt.insert()
419
420                n =  self.attachments(msg, tkt)
421                if n:
422                        attach_str = '\nThis message has %d attachment(s)\n' %(n)
423                else:
424                        attach_str = ''
425
426                # Always update the description else we get two emails one for the new ticket
427                # and for the attachments. It is an ugly hack but with trac you can not add
428                # attachments without an ticket id
429                #
430                mailto = ''
431                if self.MAILTO:
432                        mailto = self.html_mailto_link(self.to_unicode(msg['subject']), tkt['id'], body_text)
433                        tkt['description'] = '%s%s%s\n{{{\n%s\n}}}\n' %(head, attach_str, mailto, body_text)
434                        comment = 'Added mailto: link + description'
435                else:
436                        tkt['description'] = '%s%s\n{{{\n%s\n}}}\n' %(head, attach_str, body_text)
437                        comment = 'Added description'
438
439                # Save the real description and other changes
440                #
441                if self.VERSION  == 0.8:
442                        tkt.save_changes(self.db, self.author, comment, when)
443                else:
444                        tkt.save_changes(self.author, comment, when)
445
446                if self.notification:
447                        self.notify(tkt, True, when)
448
449        def parse(self, fp):
450                m = email.message_from_file(fp)
451                if not m:
452                        return
453
454                if self.DEBUG > 1:        # save the entire e-mail message text
455                        self.save_email_for_debug(m)
456                        self.debug_attachments(m)
457
458                self.db = self.env.get_db_cnx()
459                self.get_author_emailaddrs(m)
460
461                if self.get_config('notification', 'smtp_enabled') in ['true']:
462                        self.notification = 1
463                else:
464                        self.notification = 0
465
466                # Must we update existing tickets
467                #
468                if self.TICKET_UPDATE > 0:
469                        if self.ticket_update(m):
470                                return True
471
472                self.new_ticket(m)
473
474        def get_body_text(self, msg):
475                """
476                put the message text in the ticket description or in the changes field.
477                message text can be plain text or html or something else
478                """
479                has_description = 0
480                ubody_text = '\n{{{\nNo plain text message\n}}}\n'
481                for part in msg.walk():
482
483                        # 'multipart/*' is a container for multipart messages
484                        #
485                        if part.get_content_maintype() == 'multipart':
486                                continue
487
488                        if part.get_content_type() == 'text/plain':
489                                # Try to decode, if fails then do not decode
490                                #
491                                body_text = part.get_payload(decode=1)         
492                                if not body_text:                       
493                                        body_text = part.get_payload(decode=0) 
494
495                                # Get contents charset (iso-8859-15 if not defined in mail headers)
496                                # UTF-8 encode body_text
497                                #
498                                charset = part.get_content_charset('iso-8859-15')
499                                ubody_text = unicode(body_text, charset).encode('utf-8')
500
501                        elif part.get_content_type() == 'text/html':
502                                ubody_text = '\n\n(see attachment for HTML mail message)\n'
503
504                        else:
505                                ubody_text = '\n\n(see attachment for message)\n'
506
507                        has_description = 1
508                        break           # we have the description, so break
509
510                if not has_description:
511                        ubody_text = '\n\n(see attachment for message)\n'
512
513                return ubody_text
514
515        def notify(self, tkt , new=True, modtime=0):
516                """
517                A wrapper for the TRAC notify function. So we can use templates
518                """
519                try:
520                        # create false {abs_}href properties, to trick Notify()
521                        #
522                        self.env.abs_href = Href(self.get_config('project', 'url'))
523                        self.env.href = Href(self.get_config('project', 'url'))
524
525                        tn = TicketNotifyEmail(self.env)
526                        if self.notify_template:
527                                tn.template_name = self.notify_template;
528
529                        tn.notify(tkt, new, modtime)
530
531                except Exception, e:
532                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(tkt['id'], e)
533
534        def mail_line(self, str):
535                return '%s %s' % (self.comment, str)
536
537
538        def html_mailto_link(self, subject, id, body):
539                if not self.author:
540                        author = self.mail_addr
541                else:   
542                        author = self.to_unicode(self.author)
543
544                # Must find a fix
545                #
546                #arr = string.split(body, '\n')
547                #arr = map(self.mail_line, arr)
548                #body = string.join(arr, '\n')
549                #body = '%s wrote:\n%s' %(author, body)
550
551                # Temporary fix
552                str = 'mailto:%s?Subject=%s&Cc=%s' %(
553                       urllib.quote(self.email_addr),
554                           urllib.quote('Re: #%s: %s' %(id, subject)),
555                           urllib.quote(self.MAILTO_CC)
556                           )
557
558                str = '\n{{{\n#!html\n<a href="%s">Reply to: %s</a>\n}}}\n' %(str, author)
559
560                return str
561
562        def attachments(self, message, ticket):
563                '''
564                save any attachments as files in the ticket's directory
565                '''
566                count = 0
567                first = 0
568                number = 0
569                for part in message.walk():
570                        if part.get_content_maintype() == 'multipart':          # multipart/* is just a container
571                                continue
572
573                        if not first:                                                                           # first content is the message
574                                first = 1
575                                if part.get_content_type() == 'text/plain':             # if first is text, is was already put in the description
576                                        continue
577
578                        filename = part.get_filename()
579                        count = count + 1
580                        if not filename:
581                                number = number + 1
582                                filename = 'part%04d' % number
583
584                                ext = mimetypes.guess_extension(part.get_content_type())
585                                if not ext:
586                                        ext = '.bin'
587
588                                filename = '%s%s' % (filename, ext)
589                        else:
590                                filename = self.to_unicode(filename)
591
592                        # From the trac code
593                        #
594                        filename = filename.replace('\\', '/').replace(':', '/')
595                        filename = os.path.basename(filename)
596
597                        # We try to normalize the filename to utf-8 NFC if we can.
598                        # Files uploaded from OS X might be in NFD.
599                        #
600                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
601                                filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
602
603                        url_filename = urllib.quote(filename)
604                        if self.VERSION == 0.8:
605                                dir = os.path.join(self.env.get_attachments_dir(), 'ticket',
606                                                        urllib.quote(str(ticket['id'])))
607                                if not os.path.exists(dir):
608                                        mkdir_p(dir, 0755)
609                        else:
610                                dir = '/tmp'
611
612                        path, fd =  util.create_unique_file(os.path.join(dir, url_filename))
613                        text = part.get_payload(decode=1)
614                        if not text:
615                                text = '(None)'
616                        fd.write(text)
617                        fd.close()
618
619                        # get the filesize
620                        #
621                        stats = os.lstat(path)
622                        filesize = stats[stat.ST_SIZE]
623
624                        # Insert the attachment it differs for the different TRAC versions
625                        #
626                        if self.VERSION == 0.8:
627                                cursor = self.db.cursor()
628                                try:
629                                        cursor.execute('INSERT INTO attachment VALUES("%s","%s","%s",%d,%d,"%s","%s","%s")'
630                                                %('ticket', urllib.quote(str(ticket['id'])), filename + '?format=raw', filesize,
631                                                int(time.time()),'', self.author, 'e-mail') )
632
633                                # Attachment is already known
634                                #
635                                except sqlite.IntegrityError:   
636                                        #self.db.close()
637                                        return count
638
639                                self.db.commit()
640
641                        else:
642                                fd = open(path)
643                                att = attachment.Attachment(self.env, 'ticket', ticket['id'])
644                                att.insert(url_filename, fd, filesize)
645                                fd.close()
646
647                # Return how many attachments
648                #
649                return count
650
651
652def mkdir_p(dir, mode):
653        '''do a mkdir -p'''
654
655        arr = string.split(dir, '/')
656        path = ''
657        for part in arr:
658                path = '%s/%s' % (path, part)
659                try:
660                        stats = os.stat(path)
661                except OSError:
662                        os.mkdir(path, mode)
663
664
665def ReadConfig(file, name):
666        """
667        Parse the config file
668        """
669
670        if not os.path.isfile(file):
671                print 'File %s does not exist' %file
672                sys.exit(1)
673
674        config = ConfigParser.ConfigParser()
675        try:
676                config.read(file)
677        except ConfigParser.MissingSectionHeaderError,detail:
678                print detail
679                sys.exit(1)
680
681
682        # Use given project name else use defaults
683        #
684        if name:
685                if not config.has_section(name):
686                        print "Not a valid project name: %s" %name
687                        print "Valid names: %s" %config.sections()
688                        sys.exit(1)
689
690                project =  dict()
691                for option in  config.options(name):
692                        project[option] = config.get(name, option)
693
694        else:
695                project = config.defaults()
696
697        return project
698
699
700if __name__ == '__main__':
701        # Default config file
702        #
703        configfile = '@email2trac_conf@'
704        project = ''
705        component = ''
706        ENABLE_SYSLOG = 0
707               
708        try:
709                opts, args = getopt.getopt(sys.argv[1:], 'chf:p:', ['component=','help', 'file=', 'project='])
710        except getopt.error,detail:
711                print __doc__
712                print detail
713                sys.exit(1)
714       
715        project_name = None
716        for opt,value in opts:
717                if opt in [ '-h', '--help']:
718                        print __doc__
719                        sys.exit(0)
720                elif opt in ['-c', '--component']:
721                        component = value
722                elif opt in ['-f', '--file']:
723                        configfile = value
724                elif opt in ['-p', '--project']:
725                        project_name = value
726       
727        settings = ReadConfig(configfile, project_name)
728        if not settings.has_key('project'):
729                print __doc__
730                print 'No Trac project is defined in the email2trac config file.'
731                sys.exit(1)
732       
733        if component:
734                settings['component'] = component
735       
736        if settings.has_key('trac_version'):
737                version = float(settings['trac_version'])
738        else:
739                version = trac_default_version
740
741        if settings.has_key('enable_syslog'):
742                ENABLE_SYSLOG =  float(settings['enable_syslog'])
743                       
744        #debug HvB
745        #print settings
746       
747        try:
748                if version == 0.8:
749                        from trac.Environment import Environment
750                        from trac.Ticket import Ticket
751                        from trac.Notify import TicketNotifyEmail
752                        from trac.Href import Href
753                        from trac import util
754                        import sqlite
755                elif version == 0.9:
756                        from trac import attachment
757                        from trac.env import Environment
758                        from trac.ticket import Ticket
759                        from trac.web.href import Href
760                        from trac import util
761                        from trac.Notify import TicketNotifyEmail
762                elif version == 0.10:
763                        from trac import attachment
764                        from trac.env import Environment
765                        from trac.ticket import Ticket
766                        from trac.web.href import Href
767                        from trac import util
768                        # see http://projects.edgewall.com/trac/changeset/2799
769                        from trac.ticket.notification import TicketNotifyEmail
770       
771                env = Environment(settings['project'], create=0)
772                tktparser = TicketEmailParser(env, settings, version)
773                tktparser.parse(sys.stdin)
774
775        # Catch all errors ans log to SYSLOG if we have enabled this
776        # else stdout
777        #
778        except Exception, error:
779                import syslog, traceback
780
781                if ENABLE_SYSLOG:
782                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
783                        etype, evalue, etb = sys.exc_info()
784                        for e in traceback.format_exception(etype, evalue, etb):
785                                syslog.syslog(e)
786                        syslog.closelog()
787                else:
788                        traceback.print_exc()
789
790# EOB
Note: See TracBrowser for help on using the repository browser.