source: trunk/email2trac.py.in @ 276

Last change on this file since 276 was 276, checked in by bas, 15 years ago

email2trac.py.in:

  • updated default trac version for email2trac to 0.11

debian/changelog, ChangeLog?, email2trac.spec:

  • updated for release
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 41.0 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_all 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_all    : 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.9             # OPTIONAL, default is 0.10
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                -f,--file  <configuration file>
70                -n,--dry-run
71                -p, --project <project name>
72                -t, --ticket_prefix <name>
73
74SVN Info:
75        $Id: email2trac.py.in 276 2009-08-20 10:28:06Z bas $
76"""
77import os
78import sys
79import string
80import getopt
81import stat
82import time
83import email
84import email.Iterators
85import email.Header
86import re
87import urllib
88import unicodedata
89from stat import *
90import mimetypes
91import traceback
92
93
94# Will fail where unavailable, e.g. Windows
95#
96try:
97    import syslog
98    SYSLOG_AVAILABLE = True
99except ImportError:
100    SYSLOG_AVAILABLE = False
101
102from datetime import tzinfo, timedelta, datetime
103from trac import config as trac_config
104
105# Some global variables
106#
107trac_default_version = '0.11'
108m = None
109
110# A UTC class needed for trac version 0.11, added by
111# tbaschak at ktc dot mb dot ca
112#
113class UTC(tzinfo):
114        """UTC"""
115        ZERO = timedelta(0)
116        HOUR = timedelta(hours=1)
117       
118        def utcoffset(self, dt):
119                return self.ZERO
120               
121        def tzname(self, dt):
122                return "UTC"
123               
124        def dst(self, dt):
125                return self.ZERO
126
127
128class TicketEmailParser(object):
129        env = None
130        comment = '> '
131   
132        def __init__(self, env, parameters, version):
133                self.env = env
134
135                # Database connection
136                #
137                self.db = None
138
139                # Save parameters
140                #
141                self.parameters = parameters
142
143                # Some useful mail constants
144                #
145                self.author = None
146                self.email_addr = None
147                self.email_from = None
148                self.id = None
149
150                self.VERSION = version
151                self.DRY_RUN = parameters['dry_run']
152
153                self.get_config = self.env.config.get
154
155                if parameters.has_key('umask'):
156                        os.umask(int(parameters['umask'], 8))
157
158                if parameters.has_key('quote_attachment_filenames'):
159                        self.QUOTE_ATTACHMENT_FILENAMES = int(parameters['quote_attachment_filenames'])
160                else:
161                        self.QUOTE_ATTACHMENT_FILENAMES = 1
162
163                if parameters.has_key('debug'):
164                        self.DEBUG = int(parameters['debug'])
165                else:
166                        self.DEBUG = 0
167
168                if parameters.has_key('mailto_link'):
169                        self.MAILTO = int(parameters['mailto_link'])
170                        if parameters.has_key('mailto_cc'):
171                                self.MAILTO_CC = parameters['mailto_cc']
172                        else:
173                                self.MAILTO_CC = ''
174                else:
175                        self.MAILTO = 0
176
177                if parameters.has_key('spam_level'):
178                        self.SPAM_LEVEL = int(parameters['spam_level'])
179                else:
180                        self.SPAM_LEVEL = 0
181
182                if parameters.has_key('spam_header'):
183                        self.SPAM_HEADER = parameters['spam_header']
184                else:
185                        self.SPAM_HEADER = 'X-Spam-Score'
186
187                if parameters.has_key('email_quote'):
188                        self.EMAIL_QUOTE = str(parameters['email_quote'])
189                else:   
190                        self.EMAIL_QUOTE = '> '
191
192                if parameters.has_key('email_header'):
193                        self.EMAIL_HEADER = int(parameters['email_header'])
194                else:
195                        self.EMAIL_HEADER = 0
196
197                if parameters.has_key('alternate_notify_template'):
198                        self.notify_template = str(parameters['alternate_notify_template'])
199                else:
200                        self.notify_template = None
201
202                if parameters.has_key('alternate_notify_template_update'):
203                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
204                else:
205                        self.notify_template_update = None
206
207                if parameters.has_key('reply_all'):
208                        self.REPLY_ALL = int(parameters['reply_all'])
209                else:
210                        self.REPLY_ALL = 0
211
212                if parameters.has_key('ticket_update'):
213                        self.TICKET_UPDATE = int(parameters['ticket_update'])
214                else:
215                        self.TICKET_UPDATE = 0
216
217                if parameters.has_key('drop_spam'):
218                        self.DROP_SPAM = int(parameters['drop_spam'])
219                else:
220                        self.DROP_SPAM = 0
221
222                if parameters.has_key('verbatim_format'):
223                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
224                else:
225                        self.VERBATIM_FORMAT = 1
226
227                if parameters.has_key('reflow'):
228                        self.REFLOW = int(parameters['reflow'])
229                else:
230                        self.REFLOW = 1
231
232                if parameters.has_key('strip_signature'):
233                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
234                else:
235                        self.STRIP_SIGNATURE = 0
236
237                if parameters.has_key('strip_quotes'):
238                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
239                else:
240                        self.STRIP_QUOTES = 0
241
242                if parameters.has_key('use_textwrap'):
243                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
244                else:
245                        self.USE_TEXTWRAP = 0
246
247                if parameters.has_key('binhex'):
248                        self.BINHEX = parameters['binhex']
249                else:
250                        self.BINHEX = 'warn'
251
252                if parameters.has_key('applesingle'):
253                        self.APPLESINGLE = parameters['applesingle']
254                else:
255                        self.APPLESINGLE = 'warn'
256
257                if parameters.has_key('appledouble'):
258                        self.APPLEDOUBLE = parameters['appledouble']
259                else:
260                        self.APPLEDOUBLE = 'warn'
261
262                if parameters.has_key('python_egg_cache'):
263                        self.python_egg_cache = str(parameters['python_egg_cache'])
264                        os.environ['PYTHON_EGG_CACHE'] = self.python_egg_cache
265
266                self.WORKFLOW = None
267                if parameters.has_key('workflow'):
268                        self.WORKFLOW = parameters['workflow']
269
270                # Use OS independend functions
271                #
272                self.TMPDIR = os.path.normcase('/tmp')
273                if parameters.has_key('tmpdir'):
274                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
275
276                if parameters.has_key('ignore_trac_user_settings'):
277                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
278                else:
279                        self.IGNORE_TRAC_USER_SETTINGS = 0
280
281        def spam(self, message):
282                """
283                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
284                # Note if Spam_level then '*' are included
285                """
286                spam = False
287                if message.has_key(self.SPAM_HEADER):
288                        spam_l = string.split(message[self.SPAM_HEADER])
289
290                        try:
291                                number = spam_l[0].count('*')
292                        except IndexError, detail:
293                                number = 0
294                               
295                        if number >= self.SPAM_LEVEL:
296                                spam = True
297                               
298                # treat virus mails as spam
299                #
300                elif message.has_key('X-Virus-found'):                 
301                        spam = True
302
303                # How to handle SPAM messages
304                #
305                if self.DROP_SPAM and spam:
306                        if self.DEBUG > 2 :
307                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
308
309                        return 'drop'   
310
311                elif spam:
312
313                        return 'Spam'   
314
315                else:
316
317                        return False
318
319        def email_header_acl(self, keyword, header_field, default):
320                """
321                This function wil check if the email address is allowed or denied
322                to send mail to the ticket list
323            """
324                try:
325                        mail_addresses = self.parameters[keyword]
326
327                        # Check if we have an empty string
328                        #
329                        if not mail_addresses:
330                                return default
331
332                except KeyError, detail:
333                        if self.DEBUG > 2 :
334                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
335
336                        return default
337
338                mail_addresses = string.split(mail_addresses, ',')
339
340                for entry in mail_addresses:
341                        entry = entry.strip()
342                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
343                        result =  TO_RE.search(header_field)
344                        if result:
345                                return True
346
347                return False
348
349        def email_to_unicode(self, message_str):
350                """
351                Email has 7 bit ASCII code, convert it to unicode with the charset
352        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
353                understands it.
354                """
355                results =  email.Header.decode_header(message_str)
356                str = None
357                for text,format in results:
358                        if format:
359                                try:
360                                        temp = unicode(text, format)
361                                except UnicodeError, detail:
362                                        # This always works
363                                        #
364                                        temp = unicode(text, 'iso-8859-15')
365                                except LookupError, detail:
366                                        #text = 'ERROR: Could not find charset: %s, please install' %format
367                                        #temp = unicode(text, 'iso-8859-15')
368                                        temp = message_str
369                                       
370                        else:
371                                temp = string.strip(text)
372                                temp = unicode(text, 'iso-8859-15')
373
374                        if str:
375                                str = '%s %s' %(str, temp)
376                        else:
377                                str = '%s' %temp
378
379                #str = str.encode('utf-8')
380                return str
381
382        def debug_body(self, message_body, tempfile=False):
383                if tempfile:
384                        import tempfile
385                        body_file = tempfile.mktemp('.email2trac')
386                else:
387                        body_file = os.path.join(self.TMPDIR, 'body.txt')
388
389                print 'TD: writing body (%s)' % body_file
390                fx = open(body_file, 'wb')
391                if not message_body:
392                        message_body = '(None)'
393
394                message_body = message_body.encode('utf-8')
395                #message_body = unicode(message_body, 'iso-8859-15')
396
397                fx.write(message_body)
398                fx.close()
399                try:
400                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
401                except OSError:
402                        pass
403
404        def debug_attachments(self, message_parts):
405                n = 0
406                for part in message_parts:
407                        # Skip inline text parts
408                        if not isinstance(part, tuple):
409                                continue
410                               
411                        (original, filename, part) = part
412
413                        n = n + 1
414                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
415                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
416
417                        part_file = os.path.join(self.TMPDIR, filename)
418                        #part_file = '/var/tmp/part%d' % n
419                        print 'TD: writing part%d (%s)' % (n,part_file)
420                        fx = open(part_file, 'wb')
421                        text = part.get_payload(decode=1)
422                        if not text:
423                                text = '(None)'
424                        fx.write(text)
425                        fx.close()
426                        try:
427                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
428                        except OSError:
429                                pass
430
431        def email_header_txt(self, m):
432                """
433                Display To and CC addresses in description field
434                """
435                str = ''
436                #if m['To'] and len(m['To']) > 0 and m['To'] != 'hic@sara.nl':
437                if m['To'] and len(m['To']) > 0:
438                        str = "'''To:''' %s\r\n" %(m['To'])
439                if m['Cc'] and len(m['Cc']) > 0:
440                        str = "%s'''Cc:''' %s\r\n" % (str, m['Cc'])
441
442                return  self.email_to_unicode(str)
443
444
445        def get_sender_info(self, message):
446                """
447                Get the default author name and email address from the message
448                """
449
450                self.email_to = self.email_to_unicode(message['to'])
451                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
452
453                self.email_from = self.email_to_unicode(message['from'])
454                self.author, self.email_addr  = email.Utils.parseaddr(self.email_from)
455
456                # Trac can not handle author's name that contains spaces
457                #
458                self.author = self.email_addr
459
460                if self.IGNORE_TRAC_USER_SETTINGS:
461                        return
462
463                # Is this a registered user, use email address as search key:
464                # result:
465                #   u : login name
466                #   n : Name that the user has set in the settings tab
467                #   e : email address that the user has set in the settings tab
468                #
469                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
470                        if e and (e.lower() == self.email_addr.lower()) ]
471
472                if len(users) == 1:
473                        self.email_from = users[0][0]
474                        self.author = users[0][0]
475
476        def set_reply_fields(self, ticket, message):
477                """
478                Set all the right fields for a new ticket
479                """
480
481                ## Only use name or email adress
482                #ticket['reporter'] = self.email_from
483                ticket['reporter'] = self.author
484
485
486                # Put all CC-addresses in ticket CC field
487                #
488                if self.REPLY_ALL:
489                        #tos = message.get_all('to', [])
490                        ccs = message.get_all('cc', [])
491
492                        addrs = email.Utils.getaddresses(ccs)
493                        if not addrs:
494                                return
495
496                        # Remove reporter email address if notification is
497                        # on
498                        #
499                        if self.notification:
500                                try:
501                                        addrs.remove((self.author, self.email_addr))
502                                except ValueError, detail:
503                                        pass
504
505                        for name,mail in addrs:
506                                try:
507                                        mail_list = '%s, %s' %(mail_list, mail)
508                                except UnboundLocalError, detail:
509                                        mail_list = mail
510
511                        if mail_list:
512                                ticket['cc'] = self.email_to_unicode(mail_list)
513
514        def save_email_for_debug(self, message, tempfile=False):
515                if tempfile:
516                        import tempfile
517                        msg_file = tempfile.mktemp('.email2trac')
518                else:
519                        #msg_file = '/var/tmp/msg.txt'
520                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
521
522                print 'TD: saving email to %s' % msg_file
523                fx = open(msg_file, 'wb')
524                fx.write('%s' % message)
525                fx.close()
526                try:
527                        os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
528                except OSError:
529                        pass
530
531        def str_to_dict(self, str):
532                """
533                Transfrom a str of the form [<key>=<value>]+ to dict[<key>] = <value>
534                """
535
536                fields = string.split(str, ',')
537
538                result = dict()
539                for field in fields:
540                        try:
541                                index, value = string.split(field, '=')
542
543                                # We can not change the description of a ticket via the subject
544                                # line. The description is the body of the email
545                                #
546                                if index.lower() in ['description']:
547                                        continue
548
549                                if value:
550                                        result[index.lower()] = value
551
552                        except ValueError:
553                                pass
554
555                return result
556
557        def update_ticket_fields(self, ticket, user_dict, use_default=None):
558                """
559                This will update the ticket fields. It will check if the
560                given fields are known and if the right values are specified
561                It will only update the ticket field value:
562                        - If the field is known
563                        - If the value supplied is valid for the ticket field.
564                          If not then there are two options:
565                           1) Skip the value (use_default=None)
566                           2) Set default value for field (use_default=1)
567                """
568
569                # Build a system dictionary from the ticket fields
570                # with field as index and option as value
571                #
572                sys_dict = dict()
573                for field in ticket.fields:
574                        try:
575                                sys_dict[field['name']] = field['options']
576
577                        except KeyError:
578                                sys_dict[field['name']] = None
579                                pass
580
581                # Check user supplied fields an compare them with the
582                # system one's
583                #
584                for field,value in user_dict.items():
585                        if self.DEBUG >= 10:
586                                print  'user_field\t %s = %s' %(field,value)
587
588                        if sys_dict.has_key(field):
589
590                                # Check if value is an allowed system option, if TypeError then
591                                # every value is allowed
592                                #
593                                try:
594                                        if value in sys_dict[field]:
595                                                ticket[field] = value
596                                        else:
597                                                # Must we set a default if value is not allowed
598                                                #
599                                                if use_default:
600                                                        value = self.get_config('ticket', 'default_%s' %(field) )
601                                                        ticket[field] = value
602
603                                except TypeError:
604                                        ticket[field] = value
605
606                                if self.DEBUG >= 10:
607                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
608                                       
609        def ticket_update(self, m, id, spam):
610                """
611                If the current email is a reply to an existing ticket, this function
612                will append the contents of this email to that ticket, instead of
613                creating a new one.
614                """
615                if self.DEBUG:
616                        print "TD: ticket_update: %s" %id
617
618                # Must we update ticket fields
619                #
620                update_fields = dict()
621                try:
622                        id, keywords = string.split(id, '?')
623
624                        # Skip the last ':' character
625                        #
626                        keywords = keywords[:-1]
627                        update_fields = self.str_to_dict(keywords)
628
629                        # Strip '#'
630                        #
631                        self.id = int(id[1:])
632
633                except ValueError:
634                        # Strip '#' and ':'
635                        #
636                        self.id = int(id[1:-1])
637
638
639                # When is the change committed
640                #
641                #
642                if self.VERSION == 0.11:
643                        utc = UTC()
644                        when = datetime.now(utc)
645                else:
646                        when = int(time.time())
647
648                try:
649                        tkt = Ticket(self.env, self.id, self.db)
650                except util.TracError, detail:
651                        # Not a valid ticket
652                        self.id = None
653                        return False
654
655                # reopen the ticket if it is was closed
656                # We must use the ticket workflow framework
657                #
658                if tkt['status'] in ['closed']:
659
660                        #print controller.actions['reopen']
661                        #
662                        # As reference 
663                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
664                        #
665                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
666                        #print 'controller : ', a
667                        #
668                        #b = controller.get_all_status()
669                        #print 'get all status: ', b
670                        #
671                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
672                        #print 'get_ticket_changes :', b
673
674                        if self.WORKFLOW and (self.VERSION in ['0.11']) :
675                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
676                                from trac.test import Mock, MockPerm
677
678                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
679
680                                controller = ConfigurableTicketWorkflow(self.env)
681                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
682
683                                if self.DEBUG:
684                                        print 'TD: Workflow ticket update fields: ', fields
685
686                                for key in fields.keys():
687                                        tkt[key] = fields[key]
688
689                        else:
690                                tkt['status'] = 'reopened'
691                                tkt['resolution'] = ''
692
693                # Must we update some ticket fields properties
694                #
695                if update_fields:
696                        self.update_ticket_fields(tkt, update_fields)
697
698                message_parts = self.get_message_parts(m)
699                message_parts = self.unique_attachment_names(message_parts)
700
701                if self.EMAIL_HEADER:
702                        message_parts.insert(0, self.email_header_txt(m))
703
704                body_text = self.body_text(message_parts)
705
706                if body_text.strip() or update_fields:
707                        if self.DRY_RUN:
708                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
709                        else:
710                                tkt.save_changes(self.author, body_text, when)
711
712                if self.VERSION  == 0.9:
713                        str = self.attachments(message_parts, True)
714                else:
715                        str = self.attachments(message_parts)
716
717                if self.notification and not spam:
718                        self.notify(tkt, False, when)
719
720                return True
721
722        def set_ticket_fields(self, ticket):
723                """
724                set the ticket fields to value specified
725                        - /etc/email2trac.conf with <prefix>_<field>
726                        - trac default values, trac.ini
727                """
728                user_dict = dict()
729
730                for field in ticket.fields:
731
732                        name = field['name']
733
734                        # skip some fields like resolution
735                        #
736                        if name in [ 'resolution' ]:
737                                continue
738
739                        # default trac value
740                        #
741                        if not field.get('custom'):
742                                value = self.get_config('ticket', 'default_%s' %(name) )
743                        else:
744                                value = field.get('value')
745                                options = field.get('options')
746                                if value and options and value not in options:
747                                        value = options[int(value)]
748
749                        if self.DEBUG > 10:
750                                print 'trac.ini name %s = %s' %(name, value)
751
752                        prefix = self.parameters['ticket_prefix']
753                        try:
754                                value = self.parameters['%s_%s' %(prefix, name)]
755                                if self.DEBUG > 10:
756                                        print 'email2trac.conf %s = %s ' %(name, value)
757
758                        except KeyError, detail:
759                                pass
760               
761                        if self.DEBUG:
762                                print 'user_dict[%s] = %s' %(name, value)
763
764                        user_dict[name] = value
765
766                self.update_ticket_fields(ticket, user_dict, use_default=1)
767
768                # Set status ticket
769                #`
770                ticket['status'] = 'new'
771
772
773
774        def new_ticket(self, msg, subject, spam, set_fields = None):
775                """
776                Create a new ticket
777                """
778                if self.DEBUG:
779                        print "TD: new_ticket"
780
781                tkt = Ticket(self.env)
782                self.set_ticket_fields(tkt)
783
784                # Old style setting for component, will be removed
785                #
786                if spam:
787                        tkt['component'] = 'Spam'
788
789                elif self.parameters.has_key('component'):
790                        tkt['component'] = self.parameters['component']
791
792                if not msg['Subject']:
793                        tkt['summary'] = u'(No subject)'
794                else:
795                        tkt['summary'] = subject
796
797                self.set_reply_fields(tkt, msg)
798
799                if set_fields:
800                        rest, keywords = string.split(set_fields, '?')
801
802                        if keywords:
803                                update_fields = self.str_to_dict(keywords)
804                                self.update_ticket_fields(tkt, update_fields)
805
806                # produce e-mail like header
807                #
808                head = ''
809                if self.EMAIL_HEADER > 0:
810                        head = self.email_header_txt(msg)
811                       
812                message_parts = self.get_message_parts(msg)
813                message_parts = self.unique_attachment_names(message_parts)
814               
815                if self.EMAIL_HEADER > 0:
816                        message_parts.insert(0, self.email_header_txt(msg))
817                       
818                body_text = self.body_text(message_parts)
819
820                tkt['description'] = body_text
821
822                #when = int(time.time())
823                #
824                utc = UTC()
825                when = datetime.now(utc)
826
827                if not self.DRY_RUN:
828                        self.id = tkt.insert()
829       
830                changed = False
831                comment = ''
832
833                # some routines in trac are dependend on ticket id     
834                # like alternate notify template
835                #
836                if self.notify_template:
837                        tkt['id'] = self.id
838                        changed = True
839
840                # Rewrite the description if we have mailto enabled
841                #
842                if self.MAILTO:
843                        changed = True
844                        comment = u'\nadded mailto line\n'
845                        mailto = self.html_mailto_link( m['Subject'], body_text)
846
847                        tkt['description'] = u'%s\r\n%s%s\r\n' \
848                                %(head, mailto, body_text)
849
850                str =  self.attachments(message_parts)
851                if str:
852                        changed = True
853                        comment = '%s\n%s\n' %(comment, str)
854
855                if changed:
856                        if self.DRY_RUN:
857                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
858                        else:
859                                tkt.save_changes(self.author, comment)
860                                #print tkt.get_changelog(self.db, when)
861
862                if self.notification and not spam:
863                        self.notify(tkt, True)
864
865
866        def blog(self, id):
867                """
868                The blog create/update function
869                """
870                # import the modules
871                #
872                from tracfullblog.core import FullBlogCore
873                from tracfullblog.model import BlogPost, BlogCommen
874
875                # instantiate blog core
876                blog = FullBlogCore(self.env)
877                req = ''
878               
879                if id:
880
881                        # update blog
882                        #
883                        comment = BlogComment(self.env, id)
884                        comment.author = self.author
885                        comment.comment = self.get_body_text(m)
886                        blog.create_comment(req, comment)
887
888                else:
889                        # create blog
890                        #
891                        import time
892                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
893
894                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
895                       
896                        post.author = self.author
897                        post.title = self.email_to_unicode(m['Subject'])
898                        post.body = self.get_body_text(m)
899                       
900                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
901
902
903        def parse(self, fp):
904                global m
905
906                m = email.message_from_file(fp)
907               
908                if not m:
909                        if self.DEBUG:
910                                print "TD: This is not a valid email message format"
911                        return
912                       
913                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
914                m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
915
916                if self.DEBUG > 1:        # save the entire e-mail message text
917                        message_parts = self.get_message_parts(m)
918                        message_parts = self.unique_attachment_names(message_parts)
919                        self.save_email_for_debug(m, True)
920                        body_text = self.body_text(message_parts)
921                        self.debug_body(body_text, True)
922                        self.debug_attachments(message_parts)
923
924                self.db = self.env.get_db_cnx()
925                self.get_sender_info(m)
926
927                if not self.email_header_acl('white_list', self.email_addr, True):
928                        if self.DEBUG > 1 :
929                                print 'Message rejected : %s not in white list' %(self.email_addr)
930                        return False
931
932                if self.email_header_acl('black_list', self.email_addr, False):
933                        if self.DEBUG > 1 :
934                                print 'Message rejected : %s in black list' %(self.email_addr)
935                        return False
936
937                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
938                        if self.DEBUG > 1 :
939                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
940                        return False
941
942                # If drop the message
943                #
944                if self.spam(m) == 'drop':
945                        return False
946
947                elif self.spam(m) == 'spam':
948                        spam_msg = True
949
950                else:
951                        spam_msg = False
952
953                if self.get_config('notification', 'smtp_enabled') in ['true']:
954                        self.notification = 1
955                else:
956                        self.notification = 0
957
958                # Check if  FullBlogPlugin is installed
959                #
960                blog_enabled = None
961                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
962                        blog_enabled = True
963                       
964                if not m['Subject']:
965                        return False
966                else:
967                        subject  = self.email_to_unicode(m['Subject'])         
968
969                #
970                # [hic] #1529: Re: LRZ
971                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
972                #
973                TICKET_RE = re.compile(r"""
974                        (?P<blog>blog:(?P<blog_id>\w*))
975                        |(?P<new_fields>[#][?].*)
976                        |(?P<reply>[#][\d]+:)
977                        |(?P<reply_fields>[#][\d]+\?.*?:)
978                        """, re.VERBOSE)
979
980                # Find out if this is a ticket or a blog
981                #
982                result =  TICKET_RE.search(subject)
983
984                if result:
985                        if result.group('blog') and blog_enabled:
986                                self.blog(result.group('blog_id'))
987
988                        # update ticket + fields
989                        #
990                        if result.group('reply_fields') and self.TICKET_UPDATE:
991                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
992
993                        # Update ticket
994                        #
995                        elif result.group('reply') and self.TICKET_UPDATE:
996                                self.ticket_update(m, result.group('reply'), spam_msg)
997
998                        # New ticket + fields
999                        #
1000                        elif result.group('new_fields'):
1001                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1002
1003                # Create ticket
1004                #
1005                else:
1006                        self.new_ticket(m, subject, spam_msg)
1007
1008        def strip_signature(self, text):
1009                """
1010                Strip signature from message, inspired by Mailman software
1011                """
1012                body = []
1013                for line in text.splitlines():
1014                        if line == '-- ':
1015                                break
1016                        body.append(line)
1017
1018                return ('\n'.join(body))
1019
1020        def reflow(self, text, delsp = 0):
1021                """
1022                Reflow the message based on the format="flowed" specification (RFC 3676)
1023                """
1024                flowedlines = []
1025                quotelevel = 0
1026                prevflowed = 0
1027
1028                for line in text.splitlines():
1029                        from re import match
1030                       
1031                        # Figure out the quote level and the content of the current line
1032                        m = match('(>*)( ?)(.*)', line)
1033                        linequotelevel = len(m.group(1))
1034                        line = m.group(3)
1035
1036                        # Determine whether this line is flowed
1037                        if line and line != '-- ' and line[-1] == ' ':
1038                                flowed = 1
1039                        else:
1040                                flowed = 0
1041
1042                        if flowed and delsp and line and line[-1] == ' ':
1043                                line = line[:-1]
1044
1045                        # If the previous line is flowed, append this line to it
1046                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1047                                flowedlines[-1] += line
1048                        # Otherwise, start a new line
1049                        else:
1050                                flowedlines.append('>' * linequotelevel + line)
1051
1052                        prevflowed = flowed
1053                       
1054
1055                return '\n'.join(flowedlines)
1056
1057        def strip_quotes(self, text):
1058                """
1059                Strip quotes from message by Nicolas Mendoza
1060                """
1061                body = []
1062                for line in text.splitlines():
1063                        if line.startswith(self.EMAIL_QUOTE):
1064                                continue
1065                        body.append(line)
1066
1067                return ('\n'.join(body))
1068
1069        def wrap_text(self, text, replace_whitespace = False):
1070                """
1071                Will break a lines longer then given length into several small
1072                lines of size given length
1073                """
1074                import textwrap
1075
1076                LINESEPARATOR = '\n'
1077                reformat = ''
1078
1079                for s in text.split(LINESEPARATOR):
1080                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1081                        if tmp:
1082                                reformat = '%s\n%s' %(reformat,tmp)
1083                        else:
1084                                reformat = '%s\n' %reformat
1085
1086                return reformat
1087
1088                # Python2.4 and higher
1089                #
1090                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1091                #
1092
1093
1094        def get_message_parts(self, msg):
1095                """
1096                parses the email message and returns a list of body parts and attachments
1097                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1098                """
1099                message_parts = []
1100               
1101                # This is used to figure out when we are inside an AppleDouble container
1102                # AppleDouble containers consists of two parts: Mac-specific file data, and platform-independent data
1103                # We strip away Mac-specific stuff
1104                appledouble_parts = []
1105
1106                for part in msg.walk():
1107                        if self.DEBUG:
1108                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1109                               
1110                        # Check whether we just finished processing an AppleDouble container
1111                        if part not in appledouble_parts:
1112                                appledouble_parts = []
1113
1114                        # Special handling for BinHex attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1115                        if part.get_content_type() == 'application/mac-binhex40':
1116                                if self.BINHEX == 'warn':
1117                                        message_parts.append("'''A BinHex attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1118                                        continue
1119                                elif self.BINHEX == 'drop':
1120                                        continue
1121
1122                        # Special handling for AppleSingle attachments. Options are drop (leave out with no warning), warn (and leave out), and keep
1123                        if part.get_content_type() == 'application/applefile' and not part in appledouble_parts:
1124                                if self.APPLESINGLE == 'warn':
1125                                        message_parts.append("'''An AppleSingle attachment named '%s' was ignored (use MIME encoding instead).'''" % part.get_filename())
1126                                        continue
1127                                elif self.APPLESINGLE == 'drop':
1128                                        continue
1129
1130                        # Special handling for the Mac-specific part of AppleDouble attachments. Options are strip (leave out with no warning), warn (and leave out), and keep
1131                        if part.get_content_type() == 'application/applefile':
1132                                if self.APPLEDOUBLE == 'warn':
1133                                        message_parts.append("'''The resource fork of an attachment named '%s' was removed.'''" % part.get_filename())
1134                                        continue
1135                                elif self.APPLEDOUBLE == 'strip':
1136                                        continue
1137
1138                        # If we entering an AppleDouble container, set up appledouble_parts so that we know what to do with its subparts
1139                        if part.get_content_type() == 'multipart/appledouble':
1140                                appledouble_parts = part.get_payload()
1141                                continue
1142
1143                        # Any other multipart/* is just a container for multipart messages
1144                        if part.get_content_maintype() == 'multipart':
1145                                continue
1146
1147                        # Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1148                        inline = self.inline_part(part)
1149
1150                        # Inline text parts are where the body is
1151                        if part.get_content_type() == 'text/plain' and inline:
1152                                if self.DEBUG:
1153                                        print 'TD:               Inline body part'
1154
1155                                # Try to decode, if fails then do not decode
1156                                #
1157                                body_text = part.get_payload(decode=1)
1158                                if not body_text:                       
1159                                        body_text = part.get_payload(decode=0)
1160
1161                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1162                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1163
1164                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1165                                        body_text = self.reflow(body_text, delsp == 'yes')
1166       
1167                                if self.STRIP_SIGNATURE:
1168                                        body_text = self.strip_signature(body_text)
1169
1170                                if self.STRIP_QUOTES:
1171                                        body_text = self.strip_quotes(body_text)
1172
1173                                if self.USE_TEXTWRAP:
1174                                        body_text = self.wrap_text(body_text)
1175
1176                                # Get contents charset (iso-8859-15 if not defined in mail headers)
1177                                #
1178                                charset = part.get_content_charset()
1179                                if not charset:
1180                                        charset = 'iso-8859-15'
1181
1182                                try:
1183                                        ubody_text = unicode(body_text, charset)
1184
1185                                except UnicodeError, detail:
1186                                        ubody_text = unicode(body_text, 'iso-8859-15')
1187
1188                                except LookupError, detail:
1189                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1190
1191                                if self.VERBATIM_FORMAT:
1192                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1193                                else:
1194                                        message_parts.append('%s' %ubody_text)
1195                        else:
1196                                if self.DEBUG:
1197                                        print 'TD:               Filename: %s' % part.get_filename()
1198
1199                                message_parts.append((part.get_filename(), part))
1200
1201                return message_parts
1202               
1203        def unique_attachment_names(self, message_parts):
1204                renamed_parts = []
1205                attachment_names = set()
1206                for part in message_parts:
1207                       
1208                        # If not an attachment, leave it alone
1209                        if not isinstance(part, tuple):
1210                                renamed_parts.append(part)
1211                                continue
1212                               
1213                        (filename, part) = part
1214                        # Decode the filename
1215                        if filename:
1216                                filename = self.email_to_unicode(filename)                     
1217                        # If no name, use a default one
1218                        else:
1219                                filename = 'untitled-part'
1220
1221                                # Guess the extension from the content type, use non strict mode
1222                                # some additional non-standard but commonly used MIME types
1223                                # are also recognized
1224                                #
1225                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1226                                if not ext:
1227                                        ext = '.bin'
1228
1229                                filename = '%s%s' % (filename, ext)
1230
1231                        # Discard relative paths in attachment names
1232                        filename = filename.replace('\\', '/').replace(':', '/')
1233                        filename = os.path.basename(filename)
1234
1235                        # We try to normalize the filename to utf-8 NFC if we can.
1236                        # Files uploaded from OS X might be in NFD.
1237                        # Check python version and then try it
1238                        #
1239                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1240                                try:
1241                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1242                                except TypeError:
1243                                        pass
1244                                       
1245                        if self.QUOTE_ATTACHMENT_FILENAMES:
1246                                try:
1247                                        filename = urllib.quote(filename)
1248                                except KeyError, detail:
1249                                        filename = urllib.quote(filename.encode('utf-8'))
1250
1251                        # Make the filename unique for this ticket
1252                        num = 0
1253                        unique_filename = filename
1254                        filename, ext = os.path.splitext(filename)
1255
1256                        while unique_filename in attachment_names or self.attachment_exists(unique_filename):
1257                                num += 1
1258                                unique_filename = "%s-%s%s" % (filename, num, ext)
1259                               
1260                        if self.DEBUG:
1261                                print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1262
1263                        attachment_names.add(unique_filename)
1264
1265                        renamed_parts.append((filename, unique_filename, part))
1266               
1267                return renamed_parts
1268                       
1269        def inline_part(self, part):
1270                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1271               
1272                       
1273        def attachment_exists(self, filename):
1274
1275                if self.DEBUG:
1276                        print "TD: attachment_exists: Ticket number : %s, Filename : %s" %(self.id, filename)
1277
1278                # We have no valid ticket id
1279                #
1280                if not self.id:
1281                        return False
1282
1283                try:
1284                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1285                        return True
1286                except attachment.ResourceNotFound:
1287                        return False
1288                       
1289        def body_text(self, message_parts):
1290                body_text = []
1291               
1292                for part in message_parts:
1293                        # Plain text part, append it
1294                        if not isinstance(part, tuple):
1295                                body_text.extend(part.strip().splitlines())
1296                                body_text.append("")
1297                                continue
1298                               
1299                        (original, filename, part) = part
1300                        inline = self.inline_part(part)
1301                       
1302                        if part.get_content_maintype() == 'image' and inline:
1303                                body_text.append('[[Image(%s)]]' % filename)
1304                                body_text.append("")
1305                        else:
1306                                body_text.append('[attachment:"%s"]' % filename)
1307                                body_text.append("")
1308                               
1309                body_text = '\r\n'.join(body_text)
1310                return body_text
1311
1312        def notify(self, tkt, new=True, modtime=0):
1313                """
1314                A wrapper for the TRAC notify function. So we can use templates
1315                """
1316                if self.DRY_RUN:
1317                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1318                                return
1319                try:
1320                        # create false {abs_}href properties, to trick Notify()
1321                        #
1322                        if not self.VERSION == 0.11:
1323                                self.env.abs_href = Href(self.get_config('project', 'url'))
1324                                self.env.href = Href(self.get_config('project', 'url'))
1325
1326                        tn = TicketNotifyEmail(self.env)
1327
1328                        if self.notify_template:
1329
1330                                if self.VERSION == 0.11:
1331
1332                                        from trac.web.chrome import Chrome
1333
1334                                        if self.notify_template_update and not new:
1335                                                tn.template_name = self.notify_template_update
1336                                        else:
1337                                                tn.template_name = self.notify_template
1338
1339                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1340                                               
1341                                else:
1342
1343                                        tn.template_name = self.notify_template;
1344
1345                        tn.notify(tkt, new, modtime)
1346
1347                except Exception, e:
1348                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1349
1350        def html_mailto_link(self, subject, body):
1351                """
1352                This function returns a HTML mailto tag with the ticket id and author email address
1353                """
1354                if not self.author:
1355                        author = self.email_addr
1356                else:   
1357                        author = self.author
1358
1359                # use urllib to escape the chars
1360                #
1361                str = 'mailto:%s?Subject=%s&Cc=%s' %(
1362                       urllib.quote(self.email_addr),
1363                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1364                           urllib.quote(self.MAILTO_CC)
1365                           )
1366
1367                str = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(str, author)
1368                return str
1369
1370        def attachments(self, message_parts, update=False):
1371                '''
1372                save any attachments as files in the ticket's directory
1373                '''
1374                if self.DRY_RUN:
1375                        print "DRY_RUN: no attachments saved"
1376                        return ''
1377
1378                count = 0
1379
1380                # Get Maxium attachment size
1381                #
1382                max_size = int(self.get_config('attachment', 'max_size'))
1383                status   = ''
1384               
1385                for part in message_parts:
1386                        # Skip body parts
1387                        if not isinstance(part, tuple):
1388                                continue
1389                               
1390                        (original, filename, part) = part
1391                        #
1392                        # Must be tuneables HvB
1393                        #
1394                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1395                        text = part.get_payload(decode=1)
1396                        if not text:
1397                                text = '(None)'
1398                        fd.write(text)
1399                        fd.close()
1400
1401                        # get the file_size
1402                        #
1403                        stats = os.lstat(path)
1404                        file_size = stats[stat.ST_SIZE]
1405
1406                        # Check if the attachment size is allowed
1407                        #
1408                        if (max_size != -1) and (file_size > max_size):
1409                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1410                                        %(status, original, file_size, max_size)
1411
1412                                os.unlink(path)
1413                                continue
1414                        else:
1415                                count = count + 1
1416                                       
1417                        # Insert the attachment
1418                        #
1419                        fd = open(path, 'rb')
1420                        att = attachment.Attachment(self.env, 'ticket', self.id)
1421
1422                        # This will break the ticket_update system, the body_text is vaporized
1423                        # ;-(
1424                        #
1425                        if not update:
1426                                att.author = self.author
1427                                att.description = self.email_to_unicode('Added by email2trac')
1428
1429                        att.insert(filename, fd, file_size)
1430                        #except  util.TracError, detail:
1431                        #       print detail
1432
1433                        # Remove the created temporary filename
1434                        #
1435                        fd.close()
1436                        os.unlink(path)
1437
1438                # Return how many attachments
1439                #
1440                status = 'This message has %d attachment(s)\n%s' %(count, status)
1441                return status
1442
1443
1444def mkdir_p(dir, mode):
1445        '''do a mkdir -p'''
1446
1447        arr = string.split(dir, '/')
1448        path = ''
1449        for part in arr:
1450                path = '%s/%s' % (path, part)
1451                try:
1452                        stats = os.stat(path)
1453                except OSError:
1454                        os.mkdir(path, mode)
1455
1456def ReadConfig(file, name):
1457        """
1458        Parse the config file
1459        """
1460        if not os.path.isfile(file):
1461                print 'File %s does not exist' %file
1462                sys.exit(1)
1463
1464        config = trac_config.Configuration(file)
1465
1466        # Use given project name else use defaults
1467        #
1468        if name:
1469                sections = config.sections()
1470                if not name in sections:
1471                        print "Not a valid project name: %s" %name
1472                        print "Valid names: %s" %sections
1473                        sys.exit(1)
1474
1475                project =  dict()
1476                for option, value in  config.options(name):
1477                        project[option] = value
1478
1479        else:
1480                # use some trac internals to get the defaults
1481                #
1482                project = config.parser.defaults()
1483
1484        return project
1485
1486
1487if __name__ == '__main__':
1488        # Default config file
1489        #
1490        configfile = '@email2trac_conf@'
1491        project = ''
1492        component = ''
1493        ticket_prefix = 'default'
1494        dry_run = None
1495
1496        ENABLE_SYSLOG = 0
1497
1498
1499        SHORT_OPT = 'chf:np:t:'
1500        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=']
1501
1502        try:
1503                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1504        except getopt.error,detail:
1505                print __doc__
1506                print detail
1507                sys.exit(1)
1508       
1509        project_name = None
1510        for opt,value in opts:
1511                if opt in [ '-h', '--help']:
1512                        print __doc__
1513                        sys.exit(0)
1514                elif opt in ['-c', '--component']:
1515                        component = value
1516                elif opt in ['-f', '--file']:
1517                        configfile = value
1518                elif opt in ['-n', '--dry-run']:
1519                        dry_run = True
1520                elif opt in ['-p', '--project']:
1521                        project_name = value
1522                elif opt in ['-t', '--ticket_prefix']:
1523                        ticket_prefix = value
1524       
1525        settings = ReadConfig(configfile, project_name)
1526        if not settings.has_key('project'):
1527                print __doc__
1528                print 'No Trac project is defined in the email2trac config file.'
1529                sys.exit(1)
1530       
1531        if component:
1532                settings['component'] = component
1533
1534        # The default prefix for ticket values in email2trac.conf
1535        #
1536        settings['ticket_prefix'] = ticket_prefix
1537        settings['dry_run'] = dry_run
1538       
1539        if settings.has_key('trac_version'):
1540                version = settings['trac_version']
1541        else:
1542                version = trac_default_version
1543
1544
1545        #debug HvB
1546        #print settings
1547
1548        try:
1549                if version == '0.9':
1550                        from trac import attachment
1551                        from trac.env import Environment
1552                        from trac.ticket import Ticket
1553                        from trac.web.href import Href
1554                        from trac import util
1555                        from trac.Notify import TicketNotifyEmail
1556                elif version == '0.10':
1557                        from trac import attachment
1558                        from trac.env import Environment
1559                        from trac.ticket import Ticket
1560                        from trac.web.href import Href
1561                        from trac import util
1562                        #
1563                        # return  util.text.to_unicode(str)
1564                        #
1565                        # see http://projects.edgewall.com/trac/changeset/2799
1566                        from trac.ticket.notification import TicketNotifyEmail
1567                        from trac import config as trac_config
1568                elif version == '0.11':
1569                        from trac import attachment
1570                        from trac.env import Environment
1571                        from trac.ticket import Ticket
1572                        from trac.web.href import Href
1573                        from trac import config as trac_config
1574                        from trac import util
1575
1576
1577                        #
1578                        # return  util.text.to_unicode(str)
1579                        #
1580                        # see http://projects.edgewall.com/trac/changeset/2799
1581                        from trac.ticket.notification import TicketNotifyEmail
1582                else:
1583                        print 'TRAC version %s is not supported' %version
1584                        sys.exit(1)
1585                       
1586                if settings.has_key('enable_syslog'):
1587                        if SYSLOG_AVAILABLE:
1588                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1589
1590                env = Environment(settings['project'], create=0)
1591                tktparser = TicketEmailParser(env, settings, float(version))
1592                tktparser.parse(sys.stdin)
1593
1594        # Catch all errors ans log to SYSLOG if we have enabled this
1595        # else stdout
1596        #
1597        except Exception, error:
1598                if ENABLE_SYSLOG:
1599                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1600
1601                        etype, evalue, etb = sys.exc_info()
1602                        for e in traceback.format_exception(etype, evalue, etb):
1603                                syslog.syslog(e)
1604
1605                        syslog.closelog()
1606                else:
1607                        traceback.print_exc()
1608
1609                if m:
1610                        tktparser.save_email_for_debug(m, True)
1611
1612                sys.exit(1)
1613# EOB
Note: See TracBrowser for help on using the repository browser.