source: trunk/email2trac.py.in @ 273

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

email2trac.py.in:

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